diff options
613 files changed, 17049 insertions, 5384 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index df4a3e5c3b35..bd17d6d2ece5 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -20,6 +20,7 @@ aconfig_declarations_group { java_aconfig_libraries: [ // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", + "android-sdk-flags-java", "android.adaptiveauth.flags-aconfig-java", "android.app.appfunctions.flags-aconfig-java", "android.app.contextualsearch.flags-aconfig-java", diff --git a/BROADCASTS_OWNERS b/BROADCASTS_OWNERS index 01f1f8a6ba57..f0cbe46ea402 100644 --- a/BROADCASTS_OWNERS +++ b/BROADCASTS_OWNERS @@ -1,5 +1,5 @@ # Bug component: 316181 -ctate@android.com -jsharkey@google.com +set noparent + sudheersai@google.com yamasani@google.com #{LAST_RESORT_SUGGESTION} diff --git a/android-sdk-flags/Android.bp b/android-sdk-flags/Android.bp new file mode 100644 index 000000000000..79a0b9a4f273 --- /dev/null +++ b/android-sdk-flags/Android.bp @@ -0,0 +1,30 @@ +// Copyright (C) 2024 The Android Open 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: ["frameworks_base_license"], +} + +aconfig_declarations { + name: "android-sdk-flags", + package: "android.sdk", + container: "system", + srcs: ["flags.aconfig"], +} + +java_aconfig_library { + name: "android-sdk-flags-java", + aconfig_declarations: "android-sdk-flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/android-sdk-flags/flags.aconfig b/android-sdk-flags/flags.aconfig new file mode 100644 index 000000000000..cfe298e187d1 --- /dev/null +++ b/android-sdk-flags/flags.aconfig @@ -0,0 +1,12 @@ +package: "android.sdk" +container: "system" + +flag { + name: "major_minor_versioning_scheme" + namespace: "android_sdk" + description: "Use the new SDK major.minor versioning scheme (e.g. Android 40.1) which replaces the old single-integer scheme (e.g. Android 15)." + bug: "350458259" + + # Use is_fixed_read_only because DeviceConfig may not be available when Build.VERSION_CODES is first accessed + is_fixed_read_only: true +} diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java index f20b1706129b..3577fcdf04d6 100644 --- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java +++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java @@ -194,7 +194,7 @@ public final class ClientSocketPerfTest { /** * Simple benchmark for the amount of time to send a given number of messages */ - @Test + // @Test Temporarily disabled @Parameters(method = "getParams") public void time(Config config) throws Exception { reset(); diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java index af3c405eab82..ac5710047db9 100644 --- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java +++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java @@ -198,7 +198,7 @@ public final class ServerSocketPerfTest { executor.awaitTermination(5, TimeUnit.SECONDS); } - @Test + // @Test Temporarily disabled @Parameters(method = "getParams") public void throughput(Config config) throws Exception { setup(config); diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java index 4cf46e5364ea..f01ac0247908 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/ByteBufferPerfTest.java @@ -565,7 +565,6 @@ public class ByteBufferPerfTest { } @Test - @Parameters(method = "getData") public void time_new_byteArray() throws Exception { final BenchmarkState state = mBenchmarkRule.getState(); while (state.keepRunning()) { @@ -574,7 +573,6 @@ public class ByteBufferPerfTest { } @Test - @Parameters(method = "getData") public void time_ByteBuffer_allocate() throws Exception { final BenchmarkState state = mBenchmarkRule.getState(); while (state.keepRunning()) { diff --git a/apex/blobstore/OWNERS b/apex/blobstore/OWNERS index a53bbeaa8601..676cbc7eb2a3 100644 --- a/apex/blobstore/OWNERS +++ b/apex/blobstore/OWNERS @@ -1,2 +1,5 @@ +# Bug component: 25692 +set noparent + sudheersai@google.com -yamasani@google.com +yamasani@google.com #{LAST_RESORT_SUGGESTION} diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 80db264d0f44..5f5507587f72 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -23,3 +23,10 @@ flag { description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." bug: "318731461" } + +flag { + name: "cleanup_empty_jobs" + namespace: "backstage_power" + description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis." + bug: "349688611" +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl index 96494ec28204..11d17ca749b7 100644 --- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl +++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl @@ -85,6 +85,14 @@ interface IJobCallback { */ @UnsupportedAppUsage void jobFinished(int jobId, boolean reschedule); + + /* + * Inform JobScheduler to force finish this job because the client has lost + * the job handle. jobFinished can no longer be called from the client. + * @param jobId Unique integer used to identify this job + */ + void forceJobFinished(int jobId); + /* * Inform JobScheduler of a change in the estimated transfer payload. * diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index e833bb95a302..52a761f8d486 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -34,15 +34,21 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; import android.os.RemoteException; +import android.system.SystemCleaner; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.Cleaner; /** * Contains the parameters used to configure/identify your job. You do not create this object * yourself, instead it is handed in to your application by the System. */ public class JobParameters implements Parcelable { + private static final String TAG = "JobParameters"; /** @hide */ public static final int INTERNAL_STOP_REASON_UNKNOWN = -1; @@ -306,6 +312,10 @@ public class JobParameters implements Parcelable { private int mStopReason = STOP_REASON_UNDEFINED; private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN; private String debugStopReason; // Human readable stop reason for debugging. + @Nullable + private JobCleanupCallback mJobCleanupCallback; + @Nullable + private Cleaner.Cleanable mCleanable; /** @hide */ public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras, @@ -326,6 +336,8 @@ public class JobParameters implements Parcelable { this.mTriggeredContentAuthorities = triggeredContentAuthorities; this.mNetwork = network; this.mJobNamespace = namespace; + this.mJobCleanupCallback = null; + this.mCleanable = null; } /** @@ -597,6 +609,8 @@ public class JobParameters implements Parcelable { mStopReason = in.readInt(); mInternalStopReason = in.readInt(); debugStopReason = in.readString(); + mJobCleanupCallback = null; + mCleanable = null; } /** @hide */ @@ -612,6 +626,54 @@ public class JobParameters implements Parcelable { this.debugStopReason = debugStopReason; } + /** @hide */ + public void initCleaner(JobCleanupCallback jobCleanupCallback) { + mJobCleanupCallback = jobCleanupCallback; + mCleanable = SystemCleaner.cleaner().register(this, mJobCleanupCallback); + } + + /** + * Lazy initialize the cleaner and enable it + * + * @hide + */ + public void enableCleaner() { + if (mJobCleanupCallback == null) { + initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId)); + } + mJobCleanupCallback.enableCleaner(); + } + + /** + * Disable the cleaner from running and unregister it + * + * @hide + */ + public void disableCleaner() { + if (mJobCleanupCallback != null) { + mJobCleanupCallback.disableCleaner(); + if (mCleanable != null) { + mCleanable.clean(); + mCleanable = null; + } + mJobCleanupCallback = null; + } + } + + /** @hide */ + @VisibleForTesting + @Nullable + public Cleaner.Cleanable getCleanable() { + return mCleanable; + } + + /** @hide */ + @VisibleForTesting + @Nullable + public JobCleanupCallback getJobCleanupCallback() { + return mJobCleanupCallback; + } + @Override public int describeContents() { return 0; @@ -647,6 +709,67 @@ public class JobParameters implements Parcelable { dest.writeString(debugStopReason); } + /** + * JobCleanupCallback is used track JobParameters leak. If the job is started + * and jobFinish is not called at the time of garbage collection of JobParameters + * instance, it is considered a job leak. Force finish the job. + * + * @hide + */ + public static class JobCleanupCallback implements Runnable { + private final IJobCallback mCallback; + private final int mJobId; + private boolean mIsCleanerEnabled; + + public JobCleanupCallback( + IJobCallback callback, + int jobId) { + mCallback = callback; + mJobId = jobId; + mIsCleanerEnabled = false; + } + + /** + * Check if the cleaner is enabled + * + * @hide + */ + public boolean isCleanerEnabled() { + return mIsCleanerEnabled; + } + + /** + * Enable the cleaner to detect JobParameter leak + * + * @hide + */ + public void enableCleaner() { + mIsCleanerEnabled = true; + } + + /** + * Disable the cleaner from running. + * + * @hide + */ + public void disableCleaner() { + mIsCleanerEnabled = false; + } + + /** @hide */ + @Override + public void run() { + if (!isCleanerEnabled()) { + return; + } + try { + mCallback.forceJobFinished(mJobId); + } catch (Exception e) { + Log.wtf(TAG, "Could not destroy running job", e); + } + } + } + public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() { @Override public JobParameters createFromParcel(Parcel in) { diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java index 79d87edff9b2..5f80c52388b4 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java @@ -165,7 +165,13 @@ public abstract class JobServiceEngine { case MSG_EXECUTE_JOB: { final JobParameters params = (JobParameters) msg.obj; try { + if (Flags.cleanupEmptyJobs()) { + params.enableCleaner(); + } boolean workOngoing = JobServiceEngine.this.onStartJob(params); + if (Flags.cleanupEmptyJobs() && !workOngoing) { + params.disableCleaner(); + } ackStartMessage(params, workOngoing); } catch (Exception e) { Log.e(TAG, "Error while executing job: " + params.getJobId()); @@ -190,6 +196,9 @@ public abstract class JobServiceEngine { IJobCallback callback = params.getCallback(); if (callback != null) { try { + if (Flags.cleanupEmptyJobs()) { + params.disableCleaner(); + } callback.jobFinished(params.getJobId(), needsReschedule); } catch (RemoteException e) { Log.e(TAG, "Error reporting job finish to system: binder has gone" + diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index be8e304a8101..ee246d84997f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -129,6 +129,8 @@ public final class JobServiceContext implements ServiceConnection { private static final String[] VERB_STRINGS = { "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED" }; + private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:"; + private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#"; // States that a job occupies while interacting with the client. static final int VERB_BINDING = 0; @@ -292,6 +294,11 @@ public final class JobServiceContext implements ServiceConnection { } @Override + public void forceJobFinished(int jobId) { + doForceJobFinished(this, jobId); + } + + @Override public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item, long downloadBytes, long uploadBytes) { doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes); @@ -762,6 +769,35 @@ public final class JobServiceContext implements ServiceConnection { } } + /** + * This method just adds traces to evaluate jobs that leak jobparameters at the client. + * It does not stop the job. + */ + void doForceJobFinished(JobCallback cb, int jobId) { + final long ident = Binder.clearCallingIdentity(); + try { + final JobStatus executing; + synchronized (mLock) { + // not the current job, presumably it has finished in some way already + if (!verifyCallerLocked(cb)) { + return; + } + + executing = getRunningJobLocked(); + } + if (executing != null && jobId == executing.getJobId()) { + final StringBuilder stateSuffix = new StringBuilder(); + stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX); + stateSuffix.append(executing.getBatteryName()); + stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER); + stateSuffix.append(executing.getJobId()); + Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString()); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId, int workId, @BytesLong long transferredBytes) { // TODO(255393346): Make sure apps call this appropriately and monitor for abuse diff --git a/api/api.go b/api/api.go index 5b7f534443fb..e9f1feebd899 100644 --- a/api/api.go +++ b/api/api.go @@ -15,7 +15,7 @@ package api import ( - "sort" + "slices" "github.com/google/blueprint/proptools" @@ -75,31 +75,25 @@ func registerBuildComponents(ctx android.RegistrationContext) { var PrepareForCombinedApisTest = android.FixtureRegisterWithContext(registerBuildComponents) -func (a *CombinedApis) bootclasspath(ctx android.ConfigAndErrorContext) []string { - return a.properties.Bootclasspath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) -} - -func (a *CombinedApis) systemServerClasspath(ctx android.ConfigAndErrorContext) []string { - return a.properties.System_server_classpath.GetOrDefault(a.ConfigurableEvaluator(ctx), nil) -} - func (a *CombinedApis) apiFingerprintStubDeps(ctx android.BottomUpMutatorContext) []string { - ret := []string{} + bootClasspath := a.properties.Bootclasspath.GetOrDefault(ctx, nil) + systemServerClasspath := a.properties.System_server_classpath.GetOrDefault(ctx, nil) + var ret []string ret = append( ret, - transformArray(a.bootclasspath(ctx), "", ".stubs")..., + transformArray(bootClasspath, "", ".stubs")..., ) ret = append( ret, - transformArray(a.bootclasspath(ctx), "", ".stubs.system")..., + transformArray(bootClasspath, "", ".stubs.system")..., ) ret = append( ret, - transformArray(a.bootclasspath(ctx), "", ".stubs.module_lib")..., + transformArray(bootClasspath, "", ".stubs.module_lib")..., ) ret = append( ret, - transformArray(a.systemServerClasspath(ctx), "", ".stubs.system_server")..., + transformArray(systemServerClasspath, "", ".stubs.system_server")..., ) return ret } @@ -129,7 +123,7 @@ type genruleProps struct { Cmd *string Dists []android.Dist Out []string - Srcs []string + Srcs proptools.Configurable[[]string] Tools []string Visibility []string } @@ -137,7 +131,7 @@ type genruleProps struct { type libraryProps struct { Name *string Sdk_version *string - Static_libs []string + Static_libs proptools.Configurable[[]string] Visibility []string Defaults []string Is_stubs_module *bool @@ -145,7 +139,7 @@ type libraryProps struct { type fgProps struct { Name *string - Srcs []string + Srcs proptools.Configurable[[]string] Visibility []string } @@ -166,7 +160,7 @@ type MergedTxtDefinition struct { // The module for the non-updatable / non-module part of the api. BaseTxt string // The list of modules that are relevant for this merged txt. - Modules []string + Modules proptools.Configurable[[]string] // The output tag for each module to use.e.g. {.public.api.txt} for current.txt ModuleTag string // public, system, module-lib or system-server @@ -190,7 +184,8 @@ func createMergedTxt(ctx android.LoadHookContext, txt MergedTxtDefinition, stubs props.Tools = []string{"metalava"} props.Out = []string{filename} props.Cmd = proptools.StringPtr(metalavaCmd + "$(in) --out $(out)") - props.Srcs = append([]string{txt.BaseTxt}, createSrcs(txt.Modules, txt.ModuleTag)...) + props.Srcs = proptools.NewSimpleConfigurable([]string{txt.BaseTxt}) + props.Srcs.Append(createSrcs(txt.Modules, txt.ModuleTag)) if doDist { props.Dists = []android.Dist{ { @@ -209,11 +204,11 @@ func createMergedTxt(ctx android.LoadHookContext, txt MergedTxtDefinition, stubs ctx.CreateModule(genrule.GenRuleFactory, &props) } -func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, system_server_modules []string) { +func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, system_server_modules proptools.Configurable[[]string]) { for _, i := range []struct { name string tag string - modules []string + modules proptools.Configurable[[]string] }{ { name: "all-modules-public-annotations", @@ -240,33 +235,39 @@ func createMergedAnnotationsFilegroups(ctx android.LoadHookContext, modules, sys } } -func createMergedPublicStubs(ctx android.LoadHookContext, modules []string) { +func createMergedPublicStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { + modules = modules.Clone() + transformConfigurableArray(modules, "", ".stubs") props := libraryProps{} props.Name = proptools.StringPtr("all-modules-public-stubs") - props.Static_libs = transformArray(modules, "", ".stubs") + props.Static_libs = modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createMergedPublicExportableStubs(ctx android.LoadHookContext, modules []string) { +func createMergedPublicExportableStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { + modules = modules.Clone() + transformConfigurableArray(modules, "", ".stubs.exportable") props := libraryProps{} props.Name = proptools.StringPtr("all-modules-public-stubs-exportable") - props.Static_libs = transformArray(modules, "", ".stubs.exportable") + props.Static_libs = modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { +func createMergedSystemStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { // First create the all-updatable-modules-system-stubs { - updatable_modules := removeAll(modules, non_updatable_modules) + updatable_modules := modules.Clone() + removeAll(updatable_modules, non_updatable_modules) + transformConfigurableArray(updatable_modules, "", ".stubs.system") props := libraryProps{} props.Name = proptools.StringPtr("all-updatable-modules-system-stubs") - props.Static_libs = transformArray(updatable_modules, "", ".stubs.system") + props.Static_libs = updatable_modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) @@ -275,10 +276,11 @@ func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { // Now merge all-updatable-modules-system-stubs and stubs from non-updatable modules // into all-modules-system-stubs. { + static_libs := transformArray(non_updatable_modules, "", ".stubs.system") + static_libs = append(static_libs, "all-updatable-modules-system-stubs") props := libraryProps{} props.Name = proptools.StringPtr("all-modules-system-stubs") - props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.system") - props.Static_libs = append(props.Static_libs, "all-updatable-modules-system-stubs") + props.Static_libs = proptools.NewSimpleConfigurable(static_libs) props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) @@ -286,13 +288,15 @@ func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { } } -func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules []string) { +func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { // First create the all-updatable-modules-system-stubs { - updatable_modules := removeAll(modules, non_updatable_modules) + updatable_modules := modules.Clone() + removeAll(updatable_modules, non_updatable_modules) + transformConfigurableArray(updatable_modules, "", ".stubs.exportable.system") props := libraryProps{} props.Name = proptools.StringPtr("all-updatable-modules-system-stubs-exportable") - props.Static_libs = transformArray(updatable_modules, "", ".stubs.exportable.system") + props.Static_libs = updatable_modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) @@ -301,10 +305,11 @@ func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules []st // Now merge all-updatable-modules-system-stubs and stubs from non-updatable modules // into all-modules-system-stubs. { + static_libs := transformArray(non_updatable_modules, "", ".stubs.exportable.system") + static_libs = append(static_libs, "all-updatable-modules-system-stubs-exportable") props := libraryProps{} props.Name = proptools.StringPtr("all-modules-system-stubs-exportable") - props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.system") - props.Static_libs = append(props.Static_libs, "all-updatable-modules-system-stubs-exportable") + props.Static_libs = proptools.NewSimpleConfigurable(static_libs) props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) @@ -315,7 +320,7 @@ func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules []st func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) { props := libraryProps{} props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs") - props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.test") + props.Static_libs = proptools.NewSimpleConfigurable(transformArray(non_updatable_modules, "", ".stubs.test")) props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) @@ -325,25 +330,27 @@ func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) { func createMergedTestExportableStubsForNonUpdatableModules(ctx android.LoadHookContext) { props := libraryProps{} props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs-exportable") - props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.test") + props.Static_libs = proptools.NewSimpleConfigurable(transformArray(non_updatable_modules, "", ".stubs.exportable.test")) props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) { +func createMergedFrameworkImpl(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { + modules = modules.Clone() // This module is for the "framework-all" module, which should not include the core libraries. - modules = removeAll(modules, core_libraries_modules) + removeAll(modules, core_libraries_modules) // Remove the modules that belong to non-updatable APEXes since those are allowed to compile // against unstable APIs. - modules = removeAll(modules, non_updatable_modules) + removeAll(modules, non_updatable_modules) // First create updatable-framework-module-impl, which contains all updatable modules. // This module compiles against module_lib SDK. { + transformConfigurableArray(modules, "", ".impl") props := libraryProps{} props.Name = proptools.StringPtr("updatable-framework-module-impl") - props.Static_libs = transformArray(modules, "", ".impl") + props.Static_libs = modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} ctx.CreateModule(java.LibraryFactory, &props) @@ -352,65 +359,74 @@ func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) { // Now create all-framework-module-impl, which contains updatable-framework-module-impl // and all non-updatable modules. This module compiles against hidden APIs. { + static_libs := transformArray(non_updatable_modules, "", ".impl") + static_libs = append(static_libs, "updatable-framework-module-impl") props := libraryProps{} props.Name = proptools.StringPtr("all-framework-module-impl") - props.Static_libs = transformArray(non_updatable_modules, "", ".impl") - props.Static_libs = append(props.Static_libs, "updatable-framework-module-impl") + props.Static_libs = proptools.NewSimpleConfigurable(static_libs) props.Sdk_version = proptools.StringPtr("core_platform") props.Visibility = []string{"//frameworks/base"} ctx.CreateModule(java.LibraryFactory, &props) } } -func createMergedFrameworkModuleLibExportableStubs(ctx android.LoadHookContext, modules []string) { +func createMergedFrameworkModuleLibExportableStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { + modules = modules.Clone() // The user of this module compiles against the "core" SDK and against non-updatable modules, // so remove to avoid dupes. - modules = removeAll(modules, core_libraries_modules) - modules = removeAll(modules, non_updatable_modules) + removeAll(modules, core_libraries_modules) + removeAll(modules, non_updatable_modules) + transformConfigurableArray(modules, "", ".stubs.exportable.module_lib") props := libraryProps{} props.Name = proptools.StringPtr("framework-updatable-stubs-module_libs_api-exportable") - props.Static_libs = transformArray(modules, "", ".stubs.exportable.module_lib") + props.Static_libs = modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules []string) { +func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { + modules = modules.Clone() // The user of this module compiles against the "core" SDK and against non-updatable modules, // so remove to avoid dupes. - modules = removeAll(modules, core_libraries_modules) - modules = removeAll(modules, non_updatable_modules) + removeAll(modules, core_libraries_modules) + removeAll(modules, non_updatable_modules) + transformConfigurableArray(modules, "", ".stubs.module_lib") props := libraryProps{} props.Name = proptools.StringPtr("framework-updatable-stubs-module_libs_api") - props.Static_libs = transformArray(modules, "", ".stubs.module_lib") + props.Static_libs = modules props.Sdk_version = proptools.StringPtr("module_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createMergedFrameworkSystemServerExportableStubs(ctx android.LoadHookContext, bootclasspath, system_server_classpath []string) { +func createMergedFrameworkSystemServerExportableStubs(ctx android.LoadHookContext, bootclasspath, system_server_classpath proptools.Configurable[[]string]) { // The user of this module compiles against the "core" SDK and against non-updatable bootclasspathModules, // so remove to avoid dupes. - bootclasspathModules := removeAll(bootclasspath, core_libraries_modules) - bootclasspathModules = removeAll(bootclasspath, non_updatable_modules) - modules := append( - // Include all the module-lib APIs from the bootclasspath libraries. - transformArray(bootclasspathModules, "", ".stubs.exportable.module_lib"), - // Then add all the system-server APIs from the service-* libraries. - transformArray(system_server_classpath, "", ".stubs.exportable.system_server")..., - ) + bootclasspathModules := bootclasspath.Clone() + removeAll(bootclasspathModules, core_libraries_modules) + removeAll(bootclasspathModules, non_updatable_modules) + transformConfigurableArray(bootclasspathModules, "", ".stubs.exportable.module_lib") + + system_server_classpath = system_server_classpath.Clone() + transformConfigurableArray(system_server_classpath, "", ".stubs.exportable.system_server") + + // Include all the module-lib APIs from the bootclasspath libraries. + // Then add all the system-server APIs from the service-* libraries. + bootclasspathModules.Append(system_server_classpath) + props := libraryProps{} props.Name = proptools.StringPtr("framework-updatable-stubs-system_server_api-exportable") - props.Static_libs = modules + props.Static_libs = bootclasspathModules props.Sdk_version = proptools.StringPtr("system_server_current") props.Visibility = []string{"//frameworks/base"} props.Is_stubs_module = proptools.BoolPtr(true) ctx.CreateModule(java.LibraryFactory, &props) } -func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules []string) { +func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules proptools.Configurable[[]string]) { props := fgProps{} props.Name = proptools.StringPtr("all-modules-public-stubs-source") props.Srcs = createSrcs(modules, "{.public.stubs.source}") @@ -418,7 +434,14 @@ func createPublicStubsSourceFilegroup(ctx android.LoadHookContext, modules []str ctx.CreateModule(android.FileGroupFactory, &props) } -func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_classpath []string, baseTxtModulePrefix, stubsTypeSuffix string, doDist bool) { +func createMergedTxts( + ctx android.LoadHookContext, + bootclasspath proptools.Configurable[[]string], + system_server_classpath proptools.Configurable[[]string], + baseTxtModulePrefix string, + stubsTypeSuffix string, + doDist bool, +) { var textFiles []MergedTxtDefinition tagSuffix := []string{".api.txt}", ".removed-api.txt}"} @@ -463,11 +486,10 @@ func createMergedTxts(ctx android.LoadHookContext, bootclasspath, system_server_ } func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { - bootclasspath := a.bootclasspath(ctx) - system_server_classpath := a.systemServerClasspath(ctx) + bootclasspath := a.properties.Bootclasspath.Clone() + system_server_classpath := a.properties.System_server_classpath.Clone() if ctx.Config().VendorConfig("ANDROID").Bool("include_nonpublic_framework_api") { - bootclasspath = append(bootclasspath, a.properties.Conditional_bootclasspath...) - sort.Strings(bootclasspath) + bootclasspath.AppendSimpleValue(a.properties.Conditional_bootclasspath) } createMergedTxts(ctx, bootclasspath, system_server_classpath, "non-updatable-", "-", false) createMergedTxts(ctx, bootclasspath, system_server_classpath, "non-updatable-exportable-", "-exportable-", true) @@ -500,8 +522,10 @@ func combinedApisModuleFactory() android.Module { // Various utility methods below. // Creates an array of ":<m><tag>" for each m in <modules>. -func createSrcs(modules []string, tag string) []string { - return transformArray(modules, ":", tag) +func createSrcs(modules proptools.Configurable[[]string], tag string) proptools.Configurable[[]string] { + result := modules.Clone() + transformConfigurableArray(result, ":", tag) + return result } // Creates an array of "<prefix><m><suffix>", for each m in <modules>. @@ -513,11 +537,23 @@ func transformArray(modules []string, prefix, suffix string) []string { return a } -func removeAll(s []string, vs []string) []string { - for _, v := range vs { - s = remove(s, v) - } - return s +// Creates an array of "<prefix><m><suffix>", for each m in <modules>. +func transformConfigurableArray(modules proptools.Configurable[[]string], prefix, suffix string) { + modules.AddPostProcessor(func(s []string) []string { + return transformArray(s, prefix, suffix) + }) +} + +func removeAll(s proptools.Configurable[[]string], vs []string) { + s.AddPostProcessor(func(s []string) []string { + a := make([]string, 0, len(s)) + for _, module := range s { + if !slices.Contains(vs, module) { + a = append(a, module) + } + } + return a + }) } func remove(s []string, v string) []string { diff --git a/core/api/current.txt b/core/api/current.txt index 56852212ab1e..5e8febebee0e 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -145,12 +145,12 @@ package android { field public static final String MANAGE_DEVICE_POLICY_AUDIO_OUTPUT = "android.permission.MANAGE_DEVICE_POLICY_AUDIO_OUTPUT"; field public static final String MANAGE_DEVICE_POLICY_AUTOFILL = "android.permission.MANAGE_DEVICE_POLICY_AUTOFILL"; field public static final String MANAGE_DEVICE_POLICY_BACKUP_SERVICE = "android.permission.MANAGE_DEVICE_POLICY_BACKUP_SERVICE"; - field @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") public static final String MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL = "android.permission.MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL"; + field public static final String MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL = "android.permission.MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL"; field public static final String MANAGE_DEVICE_POLICY_BLUETOOTH = "android.permission.MANAGE_DEVICE_POLICY_BLUETOOTH"; field public static final String MANAGE_DEVICE_POLICY_BUGREPORT = "android.permission.MANAGE_DEVICE_POLICY_BUGREPORT"; field public static final String MANAGE_DEVICE_POLICY_CALLS = "android.permission.MANAGE_DEVICE_POLICY_CALLS"; field public static final String MANAGE_DEVICE_POLICY_CAMERA = "android.permission.MANAGE_DEVICE_POLICY_CAMERA"; - field @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") public static final String MANAGE_DEVICE_POLICY_CAMERA_TOGGLE = "android.permission.MANAGE_DEVICE_POLICY_CAMERA_TOGGLE"; + field public static final String MANAGE_DEVICE_POLICY_CAMERA_TOGGLE = "android.permission.MANAGE_DEVICE_POLICY_CAMERA_TOGGLE"; field public static final String MANAGE_DEVICE_POLICY_CERTIFICATES = "android.permission.MANAGE_DEVICE_POLICY_CERTIFICATES"; field public static final String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = "android.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE"; field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = "android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION"; @@ -172,7 +172,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS = "android.permission.MANAGE_DEVICE_POLICY_MANAGED_SUBSCRIPTIONS"; field public static final String MANAGE_DEVICE_POLICY_METERED_DATA = "android.permission.MANAGE_DEVICE_POLICY_METERED_DATA"; field public static final String MANAGE_DEVICE_POLICY_MICROPHONE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE"; - field @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") public static final String MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE"; + field public static final String MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE = "android.permission.MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE"; field public static final String MANAGE_DEVICE_POLICY_MOBILE_NETWORK = "android.permission.MANAGE_DEVICE_POLICY_MOBILE_NETWORK"; field public static final String MANAGE_DEVICE_POLICY_MODIFY_USERS = "android.permission.MANAGE_DEVICE_POLICY_MODIFY_USERS"; field public static final String MANAGE_DEVICE_POLICY_MTE = "android.permission.MANAGE_DEVICE_POLICY_MTE"; @@ -8121,7 +8121,7 @@ package android.app.admin { method public boolean isLogoutEnabled(); method public boolean isManagedProfile(@NonNull android.content.ComponentName); method public boolean isMasterVolumeMuted(@NonNull android.content.ComponentName); - method @FlaggedApi("android.app.admin.flags.is_mte_policy_enforced") public static boolean isMtePolicyEnforced(); + method public static boolean isMtePolicyEnforced(); method public boolean isNetworkLoggingEnabled(@Nullable android.content.ComponentName); method public boolean isOrganizationOwnedDeviceWithManagedProfile(); method public boolean isOverrideApnEnabled(@NonNull android.content.ComponentName); @@ -8597,7 +8597,7 @@ package android.app.admin { field public static final int TAG_ADB_SHELL_CMD = 210002; // 0x33452 field public static final int TAG_ADB_SHELL_INTERACTIVE = 210001; // 0x33451 field public static final int TAG_APP_PROCESS_START = 210005; // 0x33455 - field @FlaggedApi("android.app.admin.flags.backup_service_security_log_event_enabled") public static final int TAG_BACKUP_SERVICE_TOGGLED = 210044; // 0x3347c + field public static final int TAG_BACKUP_SERVICE_TOGGLED = 210044; // 0x3347c field public static final int TAG_BLUETOOTH_CONNECTION = 210039; // 0x33477 field public static final int TAG_BLUETOOTH_DISCONNECTION = 210040; // 0x33478 field public static final int TAG_CAMERA_POLICY_SET = 210034; // 0x33472 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ba160372f51d..ede4a8957b6f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1322,7 +1322,7 @@ package android.app.admin { method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public android.os.UserHandle getDeviceOwnerUser(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public android.app.admin.DevicePolicyState getDevicePolicyState(); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public String getFinancedDeviceKioskRoleHolder(); - method @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int getMaxPolicyStorageLimit(); + method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public int getMaxPolicyStorageLimit(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_ADMIN_POLICY}) public java.util.List<java.lang.String> getPermittedAccessibilityServices(int); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_ADMIN_POLICY}) public java.util.List<java.lang.String> getPermittedInputMethodsForCurrentUser(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public java.util.List<android.os.UserHandle> getPolicyManagedProfiles(@NonNull android.os.UserHandle); @@ -1349,7 +1349,7 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void setAuditLogEventCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.admin.SecurityLog.SecurityEvent>>); method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void setDeviceProvisioningConfigApplied(); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setDpcDownloaded(boolean); - method @FlaggedApi("android.app.admin.flags.device_policy_size_tracking_enabled") @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setMaxPolicyStorageLimit(int); + method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setMaxPolicyStorageLimit(int); method @Deprecated @RequiresPermission(value=android.Manifest.permission.GRANT_PROFILE_OWNER_DEVICE_IDS_ACCESS, conditional=true) public void setProfileOwnerCanAccessDeviceIds(@NonNull android.content.ComponentName); method public void setSecondaryLockscreenEnabled(@NonNull android.content.ComponentName, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setUserProvisioningState(int, @NonNull android.os.UserHandle); @@ -10561,6 +10561,7 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean shouldDefaultToObserveMode(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeToParcel(@NonNull android.os.Parcel, int); field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.ApduServiceInfo> CREATOR; + field @FlaggedApi("android.permission.flags.wallet_role_icon_property_enabled") public static final String PROPERTY_WALLET_PREFERRED_BANNER_AND_LABEL = "android.nfc.cardemulation.PROPERTY_WALLET_PREFERRED_BANNER_AND_LABEL"; } @FlaggedApi("android.nfc.enable_nfc_mainline") public final class NfcFServiceInfo implements android.os.Parcelable { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index b83be6b86d04..b4fb4803a2b9 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -88,6 +88,7 @@ import android.util.Size; import android.view.WindowInsetsController.Appearance; import android.window.TaskSnapshot; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.LocalePicker; import com.android.internal.app.procstats.ProcessStats; import com.android.internal.os.RoSystemProperties; @@ -238,6 +239,14 @@ public class ActivityManager { private static final RateLimitingCache<List<ProcessErrorStateInfo>> mErrorProcessesCache = new RateLimitingCache<>(10, 2); + /** Rate-Limiting cache that allows no more than 100 calls to the service per second. */ + @GuardedBy("mMemoryInfoCache") + private static final RateLimitingCache<MemoryInfo> mMemoryInfoCache = + new RateLimitingCache<>(10); + /** Used to store cached results for rate-limited calls to getMemoryInfo(). */ + @GuardedBy("mMemoryInfoCache") + private static final MemoryInfo mRateLimitedMemInfo = new MemoryInfo(); + /** * Query handler for mGetCurrentUserIdCache - returns a cached value of the current foreground * user id if the backstage_power/android.app.cache_get_current_user_id flag is enabled. @@ -3510,6 +3519,19 @@ public class ActivityManager { foregroundAppThreshold = source.readLong(); } + /** @hide */ + public void copyTo(MemoryInfo other) { + other.advertisedMem = advertisedMem; + other.availMem = availMem; + other.totalMem = totalMem; + other.threshold = threshold; + other.lowMemory = lowMemory; + other.hiddenAppThreshold = hiddenAppThreshold; + other.secondaryServerThreshold = secondaryServerThreshold; + other.visibleAppThreshold = visibleAppThreshold; + other.foregroundAppThreshold = foregroundAppThreshold; + } + public static final @android.annotation.NonNull Creator<MemoryInfo> CREATOR = new Creator<MemoryInfo>() { public MemoryInfo createFromParcel(Parcel source) { @@ -3536,6 +3558,20 @@ public class ActivityManager { * manage its memory. */ public void getMemoryInfo(MemoryInfo outInfo) { + if (Flags.rateLimitGetMemoryInfo()) { + synchronized (mMemoryInfoCache) { + mMemoryInfoCache.get(() -> { + getMemoryInfoInternal(mRateLimitedMemInfo); + return mRateLimitedMemInfo; + }); + mRateLimitedMemInfo.copyTo(outInfo); + } + } else { + getMemoryInfoInternal(outInfo); + } + } + + private void getMemoryInfoInternal(MemoryInfo outInfo) { try { getService().getMemoryInfo(outInfo); } catch (RemoteException e) { diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index 4c97dbbb4d01..c0c81df465e2 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -106,6 +106,17 @@ flag { flag { namespace: "backstage_power" + name: "use_app_info_not_launched" + description: "Use the notLaunched state from ApplicationInfo instead of current value" + is_fixed_read_only: true + bug: "362516211" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "backstage_power" name: "cache_get_current_user_id" description: "Add caching for getCurrentUserId" is_fixed_read_only: true @@ -114,3 +125,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "backstage_power" + name: "rate_limit_get_memory_info" + description: "Rate limit calls to getMemoryInfo using a cache" + is_fixed_read_only: true + bug: "364312431" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 0f54cb7bc35e..daa15f05d942 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -55,10 +55,8 @@ import static android.Manifest.permission.SET_TIME; import static android.Manifest.permission.SET_TIME_ZONE; import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; import static android.app.admin.flags.Flags.FLAG_DEVICE_THEFT_API_ENABLED; -import static android.app.admin.flags.Flags.FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED; import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.app.admin.flags.Flags.onboardingConsentlessBugreports; -import static android.app.admin.flags.Flags.FLAG_IS_MTE_POLICY_ENFORCED; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; @@ -4232,7 +4230,6 @@ public class DevicePolicyManager { * * @return whether MTE is currently enabled on the device. */ - @FlaggedApi(FLAG_IS_MTE_POLICY_ENFORCED) public static boolean isMtePolicyEnforced() { return Zygote.nativeSupportsMemoryTagging(); } @@ -8664,6 +8661,7 @@ public class DevicePolicyManager { * {@link DeviceAdminInfo#USES_POLICY_DISABLE_CAMERA}. */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_CAMERA, conditional = true) + @SupportsCoexistence public void setCameraDisabled(@Nullable ComponentName admin, boolean disabled) { if (mService != null) { try { @@ -10249,6 +10247,7 @@ public class DevicePolicyManager { * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_LOCK_TASK}. */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_LOCK_TASK, conditional = true) + @SupportsCoexistence public void clearPackagePersistentPreferredActivities(@Nullable ComponentName admin, String packageName) { throwIfParentInstance("clearPackagePersistentPreferredActivities"); @@ -11940,6 +11939,7 @@ public class DevicePolicyManager { * @throws SecurityException if {@code admin} is not a device or profile owner and if the caller * has not been granted the permission to set the given user restriction. */ + @SupportsCoexistence public void addUserRestriction(@NonNull ComponentName admin, @UserManager.UserRestrictionKey String key) { if (mService != null) { @@ -12021,6 +12021,7 @@ public class DevicePolicyManager { * @throws IllegalStateException if caller is not targeting Android * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or above. */ + @SupportsCoexistence public void addUserRestrictionGlobally(@NonNull @UserManager.UserRestrictionKey String key) { throwIfParentInstance("addUserRestrictionGlobally"); if (mService != null) { @@ -12076,6 +12077,7 @@ public class DevicePolicyManager { * @throws SecurityException if {@code admin} is not a device or profile owner and if the * caller has not been granted the permission to set the given user restriction. */ + @SupportsCoexistence public void clearUserRestriction(@NonNull ComponentName admin, @UserManager.UserRestrictionKey String key) { if (mService != null) { @@ -12312,6 +12314,7 @@ public class DevicePolicyManager { * @see #DELEGATION_PACKAGE_ACCESS */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_PACKAGE_STATE, conditional = true) + @SupportsCoexistence public boolean setApplicationHidden(@Nullable ComponentName admin, String packageName, boolean hidden) { if (mService != null) { @@ -12492,6 +12495,7 @@ public class DevicePolicyManager { * @throws SecurityException if {@code admin} is not a device or profile owner. */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_ACCOUNT_MANAGEMENT, conditional = true) + @SupportsCoexistence public void setAccountManagementDisabled(@Nullable ComponentName admin, String accountType, boolean disabled) { if (mService != null) { @@ -12575,10 +12579,24 @@ public class DevicePolicyManager { **/ @SystemApi public void setSecondaryLockscreenEnabled(@NonNull ComponentName admin, boolean enabled) { + setSecondaryLockscreenEnabled(admin, enabled, null); + } + + /** + * Called by the system supervision app to set whether a secondary lockscreen needs to be shown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. Null if the + * caller is not a device admin. + * @param enabled Whether or not the lockscreen needs to be shown. + * @param options A {@link PersistableBundle} to supply options to the lock screen. + * @hide + */ + public void setSecondaryLockscreenEnabled(@Nullable ComponentName admin, boolean enabled, + @Nullable PersistableBundle options) { throwIfParentInstance("setSecondaryLockscreenEnabled"); if (mService != null) { try { - mService.setSecondaryLockscreenEnabled(admin, enabled); + mService.setSecondaryLockscreenEnabled(admin, enabled, options); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -14276,6 +14294,7 @@ public class DevicePolicyManager { * @see #retrieveSecurityLogs */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_SECURITY_LOGGING, conditional = true) + @SupportsCoexistence public void setSecurityLoggingEnabled(@Nullable ComponentName admin, boolean enabled) { throwIfParentInstance("setSecurityLoggingEnabled"); try { @@ -17181,6 +17200,7 @@ public class DevicePolicyManager { * if USB data signaling fails to be enabled/disabled. */ @RequiresPermission(value = MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING, conditional = true) + @SupportsCoexistence public void setUsbDataSignalingEnabled(boolean enabled) { throwIfParentInstance("setUsbDataSignalingEnabled"); if (mService != null) { @@ -17746,7 +17766,6 @@ public class DevicePolicyManager { */ @SystemApi @RequiresPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) - @FlaggedApi(FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED) public void setMaxPolicyStorageLimit(int storageLimit) { if (mService != null) { try { @@ -17766,7 +17785,6 @@ public class DevicePolicyManager { */ @SystemApi @RequiresPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) - @FlaggedApi(FLAG_DEVICE_POLICY_SIZE_TRACKING_ENABLED) public int getMaxPolicyStorageLimit() { if (mService != null) { try { diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index d4e5c9960c2a..a4e2b8f62a23 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -303,7 +303,7 @@ interface IDevicePolicyManager { String[] getAccountTypesWithManagementDisabled(String callerPackageName); String[] getAccountTypesWithManagementDisabledAsUser(int userId, String callerPackageName, in boolean parent); - void setSecondaryLockscreenEnabled(in ComponentName who, boolean enabled); + void setSecondaryLockscreenEnabled(in ComponentName who, boolean enabled, in PersistableBundle options); boolean isSecondaryLockscreenEnabled(in UserHandle userHandle); void setPreferentialNetworkServiceConfigs( diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java index 477f2e007b33..beb93fd079d9 100644 --- a/core/java/android/app/admin/SecurityLog.java +++ b/core/java/android/app/admin/SecurityLog.java @@ -16,10 +16,7 @@ package android.app.admin; -import static android.app.admin.flags.Flags.FLAG_BACKUP_SERVICE_SECURITY_LOG_EVENT_ENABLED; - import android.Manifest; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -611,7 +608,6 @@ public class SecurityLog { * <li> [2] backup service state ({@code Integer}, 1 for enabled, 0 for disabled) * @see DevicePolicyManager#setBackupServiceEnabled(ComponentName, boolean) */ - @FlaggedApi(FLAG_BACKUP_SERVICE_SECURITY_LOG_EVENT_ENABLED) public static final int TAG_BACKUP_SERVICE_TOGGLED = SecurityLogTags.SECURITY_BACKUP_SERVICE_TOGGLED; /** diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 9ed5aa61b3e1..081dfe60d28c 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -13,6 +13,7 @@ flag { bug: "289520697" } +# Fully rolled out and must not be used. flag { name: "device_policy_size_tracking_enabled" is_exported: true @@ -22,13 +23,6 @@ flag { } flag { - name: "device_policy_size_tracking_internal_enabled" - namespace: "enterprise" - description: "Add feature to track the total policy size and have a max threshold - internal changes" - bug: "281543351" -} - -flag { name: "onboarding_bugreport_v2_enabled" is_exported: true namespace: "enterprise" @@ -44,13 +38,7 @@ flag { is_fixed_read_only: true } -flag { - name: "dedicated_device_control_enabled" - namespace: "enterprise" - description: "Allow the device management role holder to control which platform features are available on dedicated devices." - bug: "281964214" -} - +# Fully rolled out and must not be used. flag { name: "dedicated_device_control_api_enabled" is_exported: true @@ -164,12 +152,23 @@ flag { } flag { + name: "fix_race_condition_in_tie_profile_lock" + namespace: "enterprise" + description: "Fix race condition in tieProfileLockIfNecessary()" + bug: "355905501" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "quiet_mode_credential_bug_fix" namespace: "enterprise" description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped." bug: "293441361" } +# Fully rolled out and must not be used. flag { name: "assist_content_user_restriction_enabled" is_exported: true @@ -188,6 +187,7 @@ flag { } } +# Fully rolled out and must not be used. flag { name: "backup_service_security_log_event_enabled" is_exported: true @@ -196,6 +196,7 @@ flag { bug: "304999634" } +# Fully rolled out and must not be used. flag { name: "esim_management_enabled" is_exported: true @@ -213,6 +214,7 @@ flag { bug: "289515470" } +# Fully rolled out and must not be used. flag { name: "is_mte_policy_enforced" is_exported: true diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java index c4e8b4157752..c4bfae98e33d 100644 --- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java +++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java @@ -72,14 +72,32 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { * we need to have per-package app function schemas. * * <p>This schema should be set visible to callers from the package owner itself and for callers - * with {@link android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@link - * android.permission.EXECUTE_APP_FUNCTIONS} permissions. + * with {@link android.Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link + * android.Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permissions. * * @param packageName The package name to create a schema for. */ @NonNull public static AppSearchSchema createAppFunctionRuntimeSchema(@NonNull String packageName) { - return new AppSearchSchema.Builder(getRuntimeSchemaNameForPackage(packageName)) + return getAppFunctionRuntimeSchemaBuilder(getRuntimeSchemaNameForPackage(packageName)) + .addParentType(RUNTIME_SCHEMA_TYPE) + .build(); + } + + /** + * Creates a parent schema for all app function runtime schemas. + * + * <p>This schema should be set visible to the owner itself and for callers with {@link + * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@link + * android.permission.EXECUTE_APP_FUNCTIONS} permissions. + */ + public static AppSearchSchema createParentAppFunctionRuntimeSchema() { + return getAppFunctionRuntimeSchemaBuilder(RUNTIME_SCHEMA_TYPE).build(); + } + + private static AppSearchSchema.Builder getAppFunctionRuntimeSchemaBuilder( + @NonNull String schemaType) { + return new AppSearchSchema.Builder(schemaType) .addProperty( new AppSearchSchema.StringPropertyConfig.Builder(PROPERTY_FUNCTION_ID) .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL) @@ -111,9 +129,7 @@ public class AppFunctionRuntimeMetadata extends GenericDocument { .setJoinableValueType( AppSearchSchema.StringPropertyConfig .JOINABLE_VALUE_TYPE_QUALIFIED_ID) - .build()) - .addParentType(RUNTIME_SCHEMA_TYPE) - .build(); + .build()); } /** Returns the function id. This might look like "com.example.message#send_message". */ diff --git a/core/java/android/app/jank/OWNERS b/core/java/android/app/jank/OWNERS new file mode 100644 index 000000000000..806de574b071 --- /dev/null +++ b/core/java/android/app/jank/OWNERS @@ -0,0 +1,4 @@ +steventerrell@google.com +carmenjackson@google.com +jjaggi@google.com +pmuetschard@google.com
\ No newline at end of file diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 297fe8a9e691..748260bc8d5f 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -98,11 +98,11 @@ flag { } flag { - name: "camera_multiple_input_streams" - is_exported: true - namespace: "virtual_devices" - description: "Expose multiple surface for the virtual camera owner for different stream resolution" - bug: "341083465" + name: "camera_multiple_input_streams" + is_exported: true + namespace: "virtual_devices" + description: "Expose multiple surface for the virtual camera owner for different stream resolution" + bug: "341083465" } flag { @@ -113,8 +113,16 @@ flag { } flag { - name: "status_bar_and_insets" - namespace: "virtual_devices" - description: "Allow for status bar and insets on virtual devices" - bug: "350007866" + namespace: "virtual_devices" + name: "display_power_manager_apis" + description: "Make relevant PowerManager APIs display aware by default" + bug: "365042486" + is_fixed_read_only: true +} + +flag { + name: "status_bar_and_insets" + namespace: "virtual_devices" + description: "Allow for status bar and insets on virtual devices" + bug: "350007866" } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index da3cc1bda3be..031380dc1962 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -86,6 +86,7 @@ import android.util.Log; import android.util.proto.ProtoOutputStream; import com.android.internal.util.XmlUtils; +import com.android.modules.expresslog.Counter; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -12805,6 +12806,8 @@ public class Intent implements Parcelable, Cloneable { new ClipData.Item(text, htmlText, null, stream)); setClipData(clipData); if (stream != null) { + logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION, + "intents.value_explicit_uri_grant_for_send_action"); addFlags(FLAG_GRANT_READ_URI_PERMISSION); } return true; @@ -12846,6 +12849,8 @@ public class Intent implements Parcelable, Cloneable { setClipData(clipData); if (streams != null) { + logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION, + "intents.value_explicit_uri_grant_for_send_multiple_action"); addFlags(FLAG_GRANT_READ_URI_PERMISSION); } return true; @@ -12865,6 +12870,10 @@ public class Intent implements Parcelable, Cloneable { putExtra(MediaStore.EXTRA_OUTPUT, output); setClipData(ClipData.newRawUri("", output)); + + logCounterIfFlagsMissing( + FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_READ_URI_PERMISSION, + "intents.value_explicit_uri_grant_for_image_capture_action"); addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION); return true; } @@ -12873,6 +12882,12 @@ public class Intent implements Parcelable, Cloneable { return false; } + private void logCounterIfFlagsMissing(int requiredFlags, String metricId) { + if ((getFlags() & requiredFlags) != requiredFlags) { + Counter.logIncrement(metricId); + } + } + @android.ravenwood.annotation.RavenwoodThrow private Uri maybeConvertFileToContentUri(Context context, Uri uri) { if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 495ae6026805..34bea1a4df6f 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -849,6 +849,12 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public static final int PRIVATE_FLAG_EXT_CPU_OVERRIDE = 1 << 5; + /** + * Whether the app has been previously not launched + * @hide + */ + public static final int PRIVATE_FLAG_EXT_NOT_LAUNCHED = 1 << 6; + /** @hide */ @IntDef(flag = true, prefix = { "PRIVATE_FLAG_EXT_" }, value = { PRIVATE_FLAG_EXT_PROFILEABLE, @@ -857,6 +863,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK, PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS, PRIVATE_FLAG_EXT_CPU_OVERRIDE, + PRIVATE_FLAG_EXT_NOT_LAUNCHED, }) @Retention(RetentionPolicy.SOURCE) public @interface ApplicationInfoPrivateFlagsExt {} @@ -2664,6 +2671,22 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } /** + * Returns whether the app in the STOPPED state. + * @hide + */ + public boolean isStopped() { + return (flags & ApplicationInfo.FLAG_STOPPED) != 0; + } + + /** + * Returns whether the app was never launched (any process started) before. + * @hide + */ + public boolean isNotLaunched() { + return (privateFlagsExt & ApplicationInfo.PRIVATE_FLAG_EXT_NOT_LAUNCHED) != 0; + } + + /** * Checks if a changeId is enabled for the current user * @param changeId The changeId to verify * @return True of the changeId is enabled diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 8c56a9d443ab..26bb6e4df325 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -8847,6 +8847,8 @@ public abstract class PackageManager { } catch (PackageParserException e) { Log.w(TAG, "Failure to parse package archive apkFile= " +apkFile); return null; + } finally { + parser2.close(); } } diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 28534ad4516e..9eec7a4e8f71 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -426,4 +426,13 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + + +flag { + name: "caching_development_improvements" + namespace: "multiuser" + description: "System API to simplify caching implamentations" + bug: "364947162" + is_fixed_read_only: true +} diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 65148726224e..ef59e0af3a27 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -26,6 +26,10 @@ import android.database.sqlite.SQLiteClosable; import android.database.sqlite.SQLiteException; import android.os.Parcel; import android.os.Parcelable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; +import android.ravenwood.annotation.RavenwoodThrow; import dalvik.annotation.optimization.FastNative; import dalvik.system.CloseGuard; @@ -40,9 +44,8 @@ import dalvik.system.CloseGuard; * consumer for reading. * </p> */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.CursorWindow_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("CursorWindow_host") public class CursorWindow extends SQLiteClosable implements Parcelable { private static final String STATS_TAG = "CursorWindowStats"; @@ -63,48 +66,69 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { private final CloseGuard mCloseGuard; // May throw CursorWindowAllocationException + @RavenwoodRedirect private static native long nativeCreate(String name, int cursorWindowSize); // May throw CursorWindowAllocationException + @RavenwoodRedirect private static native long nativeCreateFromParcel(Parcel parcel); + @RavenwoodRedirect private static native void nativeDispose(long windowPtr); + @RavenwoodRedirect private static native void nativeWriteToParcel(long windowPtr, Parcel parcel); + @RavenwoodRedirect private static native String nativeGetName(long windowPtr); + @RavenwoodRedirect private static native byte[] nativeGetBlob(long windowPtr, int row, int column); + @RavenwoodRedirect private static native String nativeGetString(long windowPtr, int row, int column); + @RavenwoodThrow private static native void nativeCopyStringToBuffer(long windowPtr, int row, int column, CharArrayBuffer buffer); + @RavenwoodRedirect private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column); + @RavenwoodRedirect private static native boolean nativePutString(long windowPtr, String value, int row, int column); // Below native methods don't do unconstrained work, so are FastNative for performance @FastNative + @RavenwoodThrow private static native void nativeClear(long windowPtr); @FastNative + @RavenwoodRedirect private static native int nativeGetNumRows(long windowPtr); @FastNative + @RavenwoodRedirect private static native boolean nativeSetNumColumns(long windowPtr, int columnNum); @FastNative + @RavenwoodRedirect private static native boolean nativeAllocRow(long windowPtr); @FastNative + @RavenwoodThrow private static native void nativeFreeLastRow(long windowPtr); @FastNative + @RavenwoodRedirect private static native int nativeGetType(long windowPtr, int row, int column); @FastNative + @RavenwoodRedirect private static native long nativeGetLong(long windowPtr, int row, int column); @FastNative + @RavenwoodRedirect private static native double nativeGetDouble(long windowPtr, int row, int column); @FastNative + @RavenwoodRedirect private static native boolean nativePutLong(long windowPtr, long value, int row, int column); @FastNative + @RavenwoodRedirect private static native boolean nativePutDouble(long windowPtr, double value, int row, int column); @FastNative + @RavenwoodThrow private static native boolean nativePutNull(long windowPtr, int row, int column); diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 8975191b54c1..9355937b0963 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -170,6 +170,12 @@ public interface BiometricConstants { int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE = 20; /** + * Biometrics is not allowed to verify in apps. + * @hide + */ + int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; + + /** * This constant is only used by SystemUI. It notifies SystemUI that authentication was paused * because the authentication attempt was unsuccessful. * @hide diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index 9bc46b9f382a..a4f7485fcaa5 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -94,6 +94,13 @@ public class BiometricManager { BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; /** + * Biometrics is not allowed to verify in apps. + * @hide + */ + public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = + BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + + /** * A security vulnerability has been discovered and the sensor is unavailable until a * security update has addressed this issue. This error can be received if for example, * authentication was requested with {@link Authenticators#BIOMETRIC_STRONG}, but the diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig index d4d1ed22dd4e..dcb363ccf535 100644 --- a/core/java/android/net/vcn/flags.aconfig +++ b/core/java/android/net/vcn/flags.aconfig @@ -55,14 +55,4 @@ flag{ metadata { purpose: PURPOSE_BUGFIX } -} - -flag{ - name: "allow_disable_ipsec_loss_detector" - namespace: "vcn" - description: "Allow disabling IPsec packet loss detector" - bug: "336638836" - metadata { - purpose: PURPOSE_BUGFIX - } }
\ No newline at end of file diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index 07fbe4a04ff1..0541a96e990e 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -111,12 +111,15 @@ public class AppZygote { try { int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote( mAppInfo, mProcessInfo); + + final int[] sharedAppGid = { + UserHandle.getSharedAppGid(UserHandle.getAppId(mAppInfo.uid)) }; mZygote = Process.ZYGOTE_PROCESS.startChildZygote( "com.android.internal.os.AppZygoteInit", mAppInfo.processName + "_zygote", mZygoteUid, mZygoteUid, - null, // gids + sharedAppGid, // Zygote gets access to shared app GID for profiles runtimeFlags, "app_zygote", // seInfo abi, // abi diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 4bc3dbedeb94..97e9f34064ba 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -21,6 +21,8 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.AppOpsManager; import android.compat.annotation.UnsupportedAppUsage; +import android.ravenwood.annotation.RavenwoodClassLoadHook; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.ExceptionUtils; import android.util.Log; import android.util.Slog; @@ -30,11 +32,9 @@ import com.android.internal.os.BinderCallHeavyHitterWatcher; import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener; import com.android.internal.os.BinderInternal; import com.android.internal.os.BinderInternal.CallSession; -import com.android.internal.os.SomeArgs; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FunctionalUtils.ThrowingRunnable; import com.android.internal.util.FunctionalUtils.ThrowingSupplier; -import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.CriticalNative; @@ -48,7 +48,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Modifier; import java.util.concurrent.atomic.AtomicReferenceArray; -import java.util.function.Supplier; /** * Base class for a remotable object, the core part of a lightweight @@ -82,6 +81,8 @@ import java.util.function.Supplier; * * @see IBinder */ +@RavenwoodKeepWholeClass +@RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public class Binder implements IBinder { /* * Set this flag to true to detect anonymous, local or member classes @@ -292,33 +293,6 @@ public class Binder implements IBinder { sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking); } - private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood; - - @android.ravenwood.annotation.RavenwoodKeepWholeClass - private static class IdentitySupplier implements Supplier<SomeArgs> { - @Override - public SomeArgs get() { - final SomeArgs args = SomeArgs.obtain(); - // Match IPCThreadState behavior - args.arg1 = Boolean.FALSE; - args.argi1 = android.os.Process.myUid(); - args.argi2 = android.os.Process.myPid(); - return args; - } - } - - /** @hide */ - @android.ravenwood.annotation.RavenwoodKeep - public static void init$ravenwood() { - sIdentity$ravenwood = ThreadLocal.withInitial(new IdentitySupplier()); - } - - /** @hide */ - @android.ravenwood.annotation.RavenwoodKeep - public static void reset$ravenwood() { - sIdentity$ravenwood = null; - } - /** * Raw native pointer to JavaBBinderHolder object. Owned by this Java object. Not null. */ @@ -346,14 +320,8 @@ public class Binder implements IBinder { * 0 for a synchronous call. */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace public static final native int getCallingPid(); - /** @hide */ - public static final int getCallingPid$ravenwood() { - return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().argi2; - } - /** * Return the Linux UID assigned to the process that sent you the * current transaction that is being processed. This UID can be used with @@ -362,14 +330,8 @@ public class Binder implements IBinder { * incoming transaction, then its own UID is returned. */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace public static final native int getCallingUid(); - /** @hide */ - public static final int getCallingUid$ravenwood() { - return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().argi1; - } - /** * Returns {@code true} if the current thread is currently executing an * incoming transaction. @@ -377,21 +339,13 @@ public class Binder implements IBinder { * @hide */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace public static final native boolean isDirectlyHandlingTransactionNative(); - /** @hide */ - public static final boolean isDirectlyHandlingTransactionNative$ravenwood() { - // Ravenwood doesn't support IPC - return false; - } - private static boolean sIsHandlingBinderTransaction = false; /** * @hide */ - @android.ravenwood.annotation.RavenwoodKeep public static final boolean isDirectlyHandlingTransaction() { return sIsHandlingBinderTransaction || isDirectlyHandlingTransactionNative(); } @@ -400,7 +354,6 @@ public class Binder implements IBinder { * This is Test API which will be used to override output of isDirectlyHandlingTransactionNative * @hide */ - @android.ravenwood.annotation.RavenwoodKeep public static void setIsDirectlyHandlingTransactionOverride(boolean isInTransaction) { sIsHandlingBinderTransaction = isInTransaction; } @@ -412,15 +365,8 @@ public class Binder implements IBinder { * @hide */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace private static native boolean hasExplicitIdentity(); - /** @hide */ - private static boolean hasExplicitIdentity$ravenwood() { - return Preconditions.requireNonNullViaRavenwoodRule(sIdentity$ravenwood).get().arg1 - == Boolean.TRUE; - } - /** * Return the Linux UID assigned to the process that sent the transaction * currently being processed. @@ -429,7 +375,6 @@ public class Binder implements IBinder { * executing an incoming transaction and the calling identity has not been * explicitly set with {@link #clearCallingIdentity()} */ - @android.ravenwood.annotation.RavenwoodKeep public static final int getCallingUidOrThrow() { if (!isDirectlyHandlingTransaction() && !hasExplicitIdentity()) { throw new IllegalStateException( @@ -491,26 +436,8 @@ public class Binder implements IBinder { * @see #restoreCallingIdentity(long) */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace public static final native long clearCallingIdentity(); - /** @hide */ - public static final long clearCallingIdentity$ravenwood() { - final SomeArgs args = Preconditions.requireNonNullViaRavenwoodRule( - sIdentity$ravenwood).get(); - long res = ((long) args.argi1 << 32) | args.argi2; - if (args.arg1 == Boolean.TRUE) { - res |= (0x1 << 30); - } else { - res &= ~(0x1 << 30); - } - // Match IPCThreadState behavior - args.arg1 = Boolean.TRUE; - args.argi1 = android.os.Process.myUid(); - args.argi2 = android.os.Process.myPid(); - return res; - } - /** * Restore the identity of the incoming IPC on the current thread * back to a previously identity that was returned by {@link @@ -522,18 +449,8 @@ public class Binder implements IBinder { * @see #clearCallingIdentity */ @CriticalNative - @android.ravenwood.annotation.RavenwoodReplace public static final native void restoreCallingIdentity(long token); - /** @hide */ - public static final void restoreCallingIdentity$ravenwood(long token) { - final SomeArgs args = Preconditions.requireNonNullViaRavenwoodRule( - sIdentity$ravenwood).get(); - args.arg1 = ((token & (0x1 << 30)) != 0) ? Boolean.TRUE : Boolean.FALSE; - args.argi1 = (int) (token >> 32); - args.argi2 = (int) (token & ~(0x1 << 30)); - } - /** * Convenience method for running the provided action enclosed in * {@link #clearCallingIdentity}/{@link #restoreCallingIdentity}. @@ -708,16 +625,9 @@ public class Binder implements IBinder { * * @hide */ - @android.ravenwood.annotation.RavenwoodReplace @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public final native void markVintfStability(); - /** @hide */ - private void markVintfStability$ravenwood() { - // This is not useful for Ravenwood which uses local binder. - // TODO(b/361785059): Use real native libbinder. - } - /** * Use a VINTF-stability binder w/o VINTF requirements. Should be called * on a binder before it is sent out of process. @@ -736,14 +646,8 @@ public class Binder implements IBinder { * in order to prevent the process from holding on to objects longer than * it needs to. */ - @android.ravenwood.annotation.RavenwoodReplace public static final native void flushPendingCommands(); - /** @hide */ - public static final void flushPendingCommands$ravenwood() { - // Ravenwood doesn't support IPC; ignored - } - /** * Add the calling thread to the IPC thread pool. This function does * not return until the current process is exiting. @@ -801,7 +705,6 @@ public class Binder implements IBinder { * <p>If you're creating a Binder token (a Binder object without an attached interface), * you should use {@link #Binder(String)} instead. */ - @android.ravenwood.annotation.RavenwoodKeep public Binder() { this(null); } @@ -818,12 +721,9 @@ public class Binder implements IBinder { * Instead of creating multiple tokens with the same descriptor, consider adding a suffix to * help identify them. */ - @android.ravenwood.annotation.RavenwoodKeep public Binder(@Nullable String descriptor) { mObject = getNativeBBinderHolder(); - if (mObject != 0L) { - NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mObject); - } + NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mObject); if (FIND_POTENTIAL_LEAKS) { final Class<? extends Binder> klass = getClass(); @@ -842,7 +742,6 @@ public class Binder implements IBinder { * will be implemented for you to return the given owner IInterface when * the corresponding descriptor is requested. */ - @android.ravenwood.annotation.RavenwoodKeep public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) { mOwner = owner; mDescriptor = descriptor; @@ -851,7 +750,6 @@ public class Binder implements IBinder { /** * Default implementation returns an empty interface name. */ - @android.ravenwood.annotation.RavenwoodKeep public @Nullable String getInterfaceDescriptor() { return mDescriptor; } @@ -860,7 +758,6 @@ public class Binder implements IBinder { * Default implementation always returns true -- if you got here, * the object is alive. */ - @android.ravenwood.annotation.RavenwoodKeep public boolean pingBinder() { return true; } @@ -871,7 +768,6 @@ public class Binder implements IBinder { * Note that if you're calling on a local binder, this always returns true * because your process is alive if you're calling it. */ - @android.ravenwood.annotation.RavenwoodKeep public boolean isBinderAlive() { return true; } @@ -881,7 +777,6 @@ public class Binder implements IBinder { * to return the associated {@link IInterface} if it matches the requested * descriptor. */ - @android.ravenwood.annotation.RavenwoodKeep public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) { if (mDescriptor != null && mDescriptor.equals(descriptor)) { return mOwner; @@ -1080,7 +975,6 @@ public class Binder implements IBinder { * * @hide */ - @android.ravenwood.annotation.RavenwoodKeep public @Nullable String getTransactionName(int transactionCode) { return null; } @@ -1089,7 +983,6 @@ public class Binder implements IBinder { * @hide */ @VisibleForTesting - @android.ravenwood.annotation.RavenwoodKeep public final @Nullable String getTransactionTraceName(int transactionCode) { final boolean isInterfaceUserDefined = getMaxTransactionId() == 0; if (mTransactionTraceNames == null) { @@ -1127,7 +1020,6 @@ public class Binder implements IBinder { return transactionTraceName; } - @android.ravenwood.annotation.RavenwoodKeep private @NonNull String getSimpleDescriptor() { String descriptor = mDescriptor; if (descriptor == null) { @@ -1147,7 +1039,6 @@ public class Binder implements IBinder { * @return The highest user-defined transaction id of all transactions. * @hide */ - @android.ravenwood.annotation.RavenwoodKeep public int getMaxTransactionId() { return 0; } @@ -1359,14 +1250,12 @@ public class Binder implements IBinder { /** * Local implementation is a no-op. */ - @android.ravenwood.annotation.RavenwoodKeep public void linkToDeath(@NonNull DeathRecipient recipient, int flags) { } /** * Local implementation is a no-op. */ - @android.ravenwood.annotation.RavenwoodKeep public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { return true; } @@ -1394,13 +1283,8 @@ public class Binder implements IBinder { } } - @android.ravenwood.annotation.RavenwoodReplace private static native long getNativeBBinderHolder(); - private static long getNativeBBinderHolder$ravenwood() { - return 0L; - } - /** * By default, we use the calling UID since we can always trust it. */ diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java index da2eec9cbb28..b2d926044869 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -19,9 +19,9 @@ package android.os; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.TestApi; -import android.os.Handler; -import android.os.Looper; -import android.os.Trace; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; import android.util.SparseArray; @@ -51,9 +51,8 @@ import java.util.concurrent.locks.ReentrantLock; * <p>You can retrieve the MessageQueue for the current thread with * {@link Looper#myQueue() Looper.myQueue()}. */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("MessageQueue_host") public final class MessageQueue { private static final String TAG = "ConcurrentMessageQueue"; private static final boolean DEBUG = false; @@ -345,11 +344,17 @@ public final class MessageQueue { // Barriers are indicated by messages with a null target whose arg1 field carries the token. private final AtomicInteger mNextBarrierToken = new AtomicInteger(1); + @RavenwoodRedirect private static native long nativeInit(); + @RavenwoodRedirect private static native void nativeDestroy(long ptr); + @RavenwoodRedirect private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + @RavenwoodRedirect private static native void nativeWake(long ptr); + @RavenwoodRedirect private static native boolean nativeIsPolling(long ptr); + @RavenwoodRedirect private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events); MessageQueue(boolean quitAllowed) { diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index 6b9b3496d1c0..4474e7e91fdc 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -20,9 +20,9 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; -import android.os.Handler; -import android.os.Process; -import android.os.Trace; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; import android.util.SparseArray; @@ -42,9 +42,8 @@ import java.util.concurrent.atomic.AtomicLong; * <p>You can retrieve the MessageQueue for the current thread with * {@link Looper#myQueue() Looper.myQueue()}. */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("MessageQueue_host") public final class MessageQueue { private static final String TAG = "MessageQueue"; private static final boolean DEBUG = false; @@ -79,12 +78,18 @@ public final class MessageQueue { @UnsupportedAppUsage private int mNextBarrierToken; + @RavenwoodRedirect private native static long nativeInit(); + @RavenwoodRedirect private native static void nativeDestroy(long ptr); @UnsupportedAppUsage + @RavenwoodRedirect private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + @RavenwoodRedirect private native static void nativeWake(long ptr); + @RavenwoodRedirect private native static boolean nativeIsPolling(long ptr); + @RavenwoodRedirect private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); MessageQueue(boolean quitAllowed) { diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java index b24e14b0419e..f1affce58a5c 100644 --- a/core/java/android/os/LockedMessageQueue/MessageQueue.java +++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java @@ -20,8 +20,9 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; -import android.os.Handler; -import android.os.Trace; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; import android.util.SparseArray; @@ -44,9 +45,8 @@ import java.util.concurrent.atomic.AtomicLong; * <p>You can retrieve the MessageQueue for the current thread with * {@link Looper#myQueue() Looper.myQueue()}. */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("MessageQueue_host") public final class MessageQueue { private static final String TAG = "LockedMessageQueue"; private static final boolean DEBUG = false; @@ -389,12 +389,18 @@ public final class MessageQueue { @UnsupportedAppUsage private int mNextBarrierToken; + @RavenwoodRedirect private native static long nativeInit(); + @RavenwoodRedirect private native static void nativeDestroy(long ptr); @UnsupportedAppUsage + @RavenwoodRedirect private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + @RavenwoodRedirect private native static void nativeWake(long ptr); + @RavenwoodRedirect private native static boolean nativeIsPolling(long ptr); + @RavenwoodRedirect private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); MessageQueue(boolean quitAllowed) { diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 47096dbbac61..f7285523c01a 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -27,8 +27,8 @@ import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.AppOpsManager; import android.compat.annotation.UnsupportedAppUsage; +import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass; import android.ravenwood.annotation.RavenwoodReplace; import android.ravenwood.annotation.RavenwoodThrow; import android.text.TextUtils; @@ -233,8 +233,7 @@ import java.util.function.IntFunction; * {@link #readSparseArray(ClassLoader, Class)}. */ @RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.Parcel_host") +@RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class Parcel { private static final boolean DEBUG_RECYCLE = false; @@ -389,10 +388,8 @@ public final class Parcel { @CriticalNative private static native void nativeMarkSensitive(long nativePtr); @FastNative - @RavenwoodThrow private static native void nativeMarkForBinder(long nativePtr, IBinder binder); @CriticalNative - @RavenwoodThrow private static native boolean nativeIsForRpc(long nativePtr); @CriticalNative private static native int nativeDataSize(long nativePtr); @@ -424,14 +421,12 @@ public final class Parcel { private static native int nativeWriteFloat(long nativePtr, float val); @CriticalNative private static native int nativeWriteDouble(long nativePtr, double val); - @RavenwoodThrow private static native void nativeSignalExceptionForError(int error); @FastNative private static native void nativeWriteString8(long nativePtr, String val); @FastNative private static native void nativeWriteString16(long nativePtr, String val); @FastNative - @RavenwoodThrow private static native void nativeWriteStrongBinder(long nativePtr, IBinder val); @FastNative private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val); @@ -452,7 +447,6 @@ public final class Parcel { @FastNative private static native String nativeReadString16(long nativePtr); @FastNative - @RavenwoodThrow private static native IBinder nativeReadStrongBinder(long nativePtr); @FastNative private static native FileDescriptor nativeReadFileDescriptor(long nativePtr); @@ -477,17 +471,13 @@ public final class Parcel { private static native boolean nativeHasBinders(long nativePtr); private static native boolean nativeHasBindersInRange( long nativePtr, int offset, int length); - @RavenwoodThrow private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); - @RavenwoodThrow private static native void nativeEnforceInterface(long nativePtr, String interfaceName); @CriticalNative - @RavenwoodThrow private static native boolean nativeReplaceCallingWorkSourceUid( long nativePtr, int workSourceUid); @CriticalNative - @RavenwoodThrow private static native int nativeReadCallingWorkSourceUid(long nativePtr); /** Last time exception with a stack trace was written */ @@ -496,7 +486,6 @@ public final class Parcel { private static final int WRITE_EXCEPTION_STACK_TRACE_THRESHOLD_MS = 1000; @CriticalNative - @RavenwoodThrow private static native long nativeGetOpenAshmemSize(long nativePtr); public final static Parcelable.Creator<String> STRING_CREATOR @@ -660,12 +649,10 @@ public final class Parcel { /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - @RavenwoodThrow public static native long getGlobalAllocSize(); /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - @RavenwoodThrow public static native long getGlobalAllocCount(); /** @@ -1257,6 +1244,7 @@ public final class Parcel { * @hide */ @UnsupportedAppUsage + @RavenwoodThrow(blockedBy = android.text.Spanned.class) public final void writeCharSequence(@Nullable CharSequence val) { TextUtils.writeToParcel(val, this, 0); } @@ -2996,7 +2984,7 @@ public final class Parcel { * @see #writeNoException * @see #readException */ - @RavenwoodReplace + @RavenwoodReplace(blockedBy = AppOpsManager.class) public final void writeException(@NonNull Exception e) { AppOpsManager.prefixParcelWithAppOpsIfNeeded(this); @@ -3035,10 +3023,15 @@ public final class Parcel { } } - /** @hide */ - public final void writeException$ravenwood(@NonNull Exception e) { - // Ravenwood doesn't support IPC, no transaction headers needed - writeInt(getExceptionCode(e)); + private void writeException$ravenwood(@NonNull Exception e) { + int code = getExceptionCode(e); + writeInt(code); + if (code == 0) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new RuntimeException(e); + } writeString(e.getMessage()); writeInt(0); } @@ -3096,7 +3089,7 @@ public final class Parcel { * @see #writeException * @see #readException */ - @RavenwoodReplace + @RavenwoodReplace(blockedBy = AppOpsManager.class) public final void writeNoException() { AppOpsManager.prefixParcelWithAppOpsIfNeeded(this); @@ -3127,9 +3120,7 @@ public final class Parcel { } } - /** @hide */ - public final void writeNoException$ravenwood() { - // Ravenwood doesn't support IPC, no transaction headers needed + private void writeNoException$ravenwood() { writeInt(0); } diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java index 79f229acbccb..80c24a9003e8 100644 --- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java @@ -19,8 +19,9 @@ package android.os; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.TestApi; -import android.os.Handler; -import android.os.Trace; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.Printer; import android.util.SparseArray; @@ -37,8 +38,6 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.PriorityQueue; -import java.util.PriorityQueue; -import java.util.PriorityQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -50,9 +49,8 @@ import java.util.concurrent.atomic.AtomicLong; * <p>You can retrieve the MessageQueue for the current thread with * {@link Looper#myQueue() Looper.myQueue()}. */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("MessageQueue_host") public final class MessageQueue { private static final String TAG = "SemiConcurrentMessageQueue"; private static final boolean DEBUG = false; @@ -338,11 +336,17 @@ public final class MessageQueue { // Barriers are indicated by messages with a null target whose arg1 field carries the token. private final AtomicInteger mNextBarrierToken = new AtomicInteger(1); + @RavenwoodRedirect private static native long nativeInit(); + @RavenwoodRedirect private static native void nativeDestroy(long ptr); + @RavenwoodRedirect private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + @RavenwoodRedirect private static native void nativeWake(long ptr); + @RavenwoodRedirect private static native boolean nativeIsPolling(long ptr); + @RavenwoodRedirect private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events); MessageQueue(boolean quitAllowed) { diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 50b73a9d3f66..81dc46ecbd3a 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -2605,10 +2605,15 @@ public final class StrictMode { * (Java) thread-local policy value. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @android.ravenwood.annotation.RavenwoodReplace private static void onBinderStrictModePolicyChange(@ThreadPolicyMask int newPolicy) { setBlockGuardPolicy(newPolicy); } + private static void onBinderStrictModePolicyChange$ravenwood(@ThreadPolicyMask int newPolicy) { + /* no-op */ + } + /** * A tracked, critical time span. (e.g. during an animation.) * diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java index 0a386913de59..e53873b5622e 100644 --- a/core/java/android/os/SystemProperties.java +++ b/core/java/android/os/SystemProperties.java @@ -21,11 +21,13 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.util.Log; import android.util.MutableInt; import com.android.internal.annotations.GuardedBy; +import com.android.internal.ravenwood.RavenwoodEnvironment; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -56,8 +58,7 @@ import java.util.function.Predicate; */ @SystemApi @RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.SystemProperties_host") +@RavenwoodRedirectionClass("SystemProperties_host") public class SystemProperties { private static final String TAG = "SystemProperties"; private static final boolean TRACK_KEY_ACCESS = false; @@ -75,7 +76,7 @@ public class SystemProperties { @UnsupportedAppUsage @GuardedBy("sChangeCallbacks") - private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>(); + static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>(); @GuardedBy("sRoReads") private static final HashMap<String, MutableInt> sRoReads = @@ -102,30 +103,18 @@ public class SystemProperties { } /** @hide */ + @RavenwoodRedirect public static void init$ravenwood(Map<String, String> values, Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) { - native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate, - SystemProperties::callChangeCallbacks); - synchronized (sChangeCallbacks) { - sChangeCallbacks.clear(); - } + throw RavenwoodEnvironment.notSupportedOnDevice(); } /** @hide */ + @RavenwoodRedirect public static void reset$ravenwood() { - native_reset$ravenwood(); - synchronized (sChangeCallbacks) { - sChangeCallbacks.clear(); - } + throw RavenwoodEnvironment.notSupportedOnDevice(); } - // These native methods are currently only implemented by Ravenwood, as it's the only - // mechanism we have to jump to our RavenwoodNativeSubstitutionClass - private static native void native_init$ravenwood(Map<String, String> values, - Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate, - Runnable changeCallback); - private static native void native_reset$ravenwood(); - // The one-argument version of native_get used to be a regular native function. Nowadays, // we use the two-argument form of native_get all the time, but we can't just delete the // one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation @@ -137,34 +126,46 @@ public class SystemProperties { @FastNative @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @RavenwoodRedirect private static native String native_get(String key, String def); @FastNative @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @RavenwoodRedirect private static native int native_get_int(String key, int def); @FastNative @UnsupportedAppUsage + @RavenwoodRedirect private static native long native_get_long(String key, long def); @FastNative @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @RavenwoodRedirect private static native boolean native_get_boolean(String key, boolean def); @FastNative + @RavenwoodRedirect private static native long native_find(String name); @FastNative + @RavenwoodRedirect private static native String native_get(long handle); @CriticalNative + @RavenwoodRedirect private static native int native_get_int(long handle, int def); @CriticalNative + @RavenwoodRedirect private static native long native_get_long(long handle, long def); @CriticalNative + @RavenwoodRedirect private static native boolean native_get_boolean(long handle, boolean def); // _NOT_ FastNative: native_set performs IPC and can block @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @RavenwoodRedirect private static native void native_set(String key, String def); @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) + @RavenwoodRedirect private static native void native_add_change_callback(); + @RavenwoodRedirect private static native void native_report_sysprop_change(); /** @@ -300,7 +301,7 @@ public class SystemProperties { } @SuppressWarnings("unused") // Called from native code. - private static void callChangeCallbacks() { + static void callChangeCallbacks() { ArrayList<Runnable> callbacks = null; synchronized (sChangeCallbacks) { //Log.i("foo", "Calling " + sChangeCallbacks.size() + " change callbacks!"); diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 5174005a8175..6c486dbfd7a2 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -241,3 +241,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "wallet_role_icon_property_enabled" + is_exported: true + namespace: "wallet_integration" + description: "This flag is used to enabled the Wallet Role s icon fetching from manifest property" + bug: "349942654" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 98904fe246f8..0a05f704f523 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5150,13 +5150,19 @@ public final class Settings { public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; /** - * The screen backlight brightness between 0 and 255. + * The screen backlight brightness between 1 (minimum) and 255 (maximum). + * + * Use {@link android.view.WindowManager.LayoutParams#screenBrightness} to set the screen + * brightness instead. */ @Readable public static final String SCREEN_BRIGHTNESS = "screen_brightness"; /** - * Control whether to enable automatic brightness mode. + * Controls whether to enable automatic brightness mode. Value can be set to + * {@link #SCREEN_BRIGHTNESS_MODE_MANUAL} or {@link #SCREEN_BRIGHTNESS_MODE_AUTOMATIC}. + * If {@link #SCREEN_BRIGHTNESS_MODE_AUTOMATIC} is set, the system may change + * {@link #SCREEN_BRIGHTNESS} automatically. */ @Readable public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode"; diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java index 5d84d17bdb6e..bbe9cdbbce9f 100644 --- a/core/java/android/text/ClientFlags.java +++ b/core/java/android/text/ClientFlags.java @@ -35,20 +35,6 @@ public class ClientFlags { } /** - * @see Flags#phraseStrictFallback() - */ - public static boolean phraseStrictFallback() { - return TextFlags.isFeatureEnabled(Flags.FLAG_PHRASE_STRICT_FALLBACK); - } - - /** - * @see Flags#useBoundsForWidth() - */ - public static boolean useBoundsForWidth() { - return TextFlags.isFeatureEnabled(Flags.FLAG_USE_BOUNDS_FOR_WIDTH); - } - - /** * @see Flags#fixLineHeightForLocale() */ public static boolean fixLineHeightForLocale() { diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java index 9e02460d2637..0f1b031a8fad 100644 --- a/core/java/android/text/TextFlags.java +++ b/core/java/android/text/TextFlags.java @@ -56,8 +56,6 @@ public final class TextFlags { */ public static final String[] TEXT_ACONFIGS_FLAGS = { Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN, - Flags.FLAG_PHRASE_STRICT_FALLBACK, - Flags.FLAG_USE_BOUNDS_FOR_WIDTH, Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE, Flags.FLAG_ICU_BIDI_MIGRATION, Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU, @@ -70,8 +68,6 @@ public final class TextFlags { */ public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = { Flags.noBreakNoHyphenationSpan(), - Flags.phraseStrictFallback(), - Flags.useBoundsForWidth(), Flags.fixLineHeightForLocale(), Flags.icuBidiMigration(), Flags.fixMisalignedContextMenu(), diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index d33c95e06677..34b6c840dd6a 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -2,14 +2,6 @@ package: "com.android.text.flags" container: "system" flag { - name: "vendor_custom_locale_fallback" - namespace: "text" - description: "A feature flag that adds custom locale fallback to the vendor customization XML. This enables vendors to add their locale specific fonts, e.g. Japanese font." - is_fixed_read_only: true - bug: "278768958" -} - -flag { name: "new_fonts_fallback_xml" is_exported: true namespace: "text" @@ -20,13 +12,6 @@ flag { } flag { - name: "fix_double_underline" - namespace: "text" - description: "Feature flag for fixing double underline because of the multiple font used in the single line." - bug: "297336724" -} - -flag { name: "fix_line_height_for_locale" is_exported: true namespace: "text" @@ -66,13 +51,6 @@ flag { } flag { - name: "phrase_strict_fallback" - namespace: "text" - description: "Feature flag for automatic fallback from phrase based line break to strict line break." - bug: "281970875" -} - -flag { name: "use_bounds_for_width" is_exported: true namespace: "text" @@ -81,13 +59,6 @@ flag { } flag { - name: "deprecate_ui_fonts" - namespace: "text" - description: "Feature flag for deprecating UI fonts. By setting true for this feature flag, the elegant text height of will be turned on by default unless explicitly setting it to false by attribute or Java API call." - bug: "279646685" -} - -flag { name: "word_style_auto" is_exported: true namespace: "text" diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java index 0a73fd1689c3..00545da2baf2 100644 --- a/core/java/android/util/EventLog.java +++ b/core/java/android/util/EventLog.java @@ -21,6 +21,10 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; +import android.ravenwood.annotation.RavenwoodThrow; import java.io.BufferedReader; import java.io.FileReader; @@ -48,9 +52,8 @@ import java.util.regex.Pattern; * They carry a payload of one or more int, long, or String values. The * event-log-tags file defines the payload contents for each type code. */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.EventLog_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("EventLog_host") public class EventLog { /** @hide */ public EventLog() {} @@ -339,6 +342,7 @@ public class EventLog { * @param value A value to log * @return The number of bytes written */ + @RavenwoodRedirect public static native int writeEvent(int tag, int value); /** @@ -347,6 +351,7 @@ public class EventLog { * @param value A value to log * @return The number of bytes written */ + @RavenwoodRedirect public static native int writeEvent(int tag, long value); /** @@ -355,6 +360,7 @@ public class EventLog { * @param value A value to log * @return The number of bytes written */ + @RavenwoodRedirect public static native int writeEvent(int tag, float value); /** @@ -363,6 +369,7 @@ public class EventLog { * @param str A value to log * @return The number of bytes written */ + @RavenwoodRedirect public static native int writeEvent(int tag, String str); /** @@ -371,6 +378,7 @@ public class EventLog { * @param list A list of values to log * @return The number of bytes written */ + @RavenwoodRedirect public static native int writeEvent(int tag, Object... list); /** @@ -379,6 +387,7 @@ public class EventLog { * @param output container to add events into * @throws IOException if something goes wrong reading events */ + @RavenwoodThrow public static native void readEvents(int[] tags, Collection<Event> output) throws IOException; @@ -391,6 +400,7 @@ public class EventLog { * @hide */ @SystemApi + @RavenwoodThrow public static native void readEventsOnWrapping(int[] tags, long timestamp, Collection<Event> output) throws IOException; diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java index 9ecb4cbb283d..b2017a5fa868 100644 --- a/core/java/android/util/Slog.java +++ b/core/java/android/util/Slog.java @@ -224,7 +224,6 @@ public final class Slog { /** * Similar to {@link #wtf(String, String)}, but does not output anything to the log. */ - @android.ravenwood.annotation.RavenwoodThrow public static void wtfQuiet(@Nullable String tag, @NonNull String msg) { Log.wtfQuiet(Log.LOG_ID_SYSTEM, tag, msg, true); } @@ -243,7 +242,6 @@ public final class Slog { * @see Log#wtfStack(String, String) */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - @android.ravenwood.annotation.RavenwoodThrow public static int wtfStack(@Nullable String tag, @NonNull String msg) { return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, true, true); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7b4ea41554f1..0ed0e60f6b4d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -371,7 +371,7 @@ import java.util.function.Predicate; * </tr> * <tr> * <td><code>{@link #onTouchEvent(MotionEvent)}</code></td> - * <td>Called when a touch screen motion event occurs. + * <td>Called when a motion event occurs with pointers down on the view. * </td> * </tr> * <tr> @@ -17873,7 +17873,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Implement this method to handle touch screen motion events. + * Implement this method to handle pointer events. + * <p> + * This method is called to handle motion events where pointers are down on + * the view. For example, this could include touchscreen touches, stylus + * touches, or click-and-drag events from a mouse. However, it is not called + * for motion events that do not involve pointers being down, such as hover + * events or mouse scroll wheel movements. * <p> * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 3b5286a04b3d..22374173b988 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -4504,6 +4504,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; + if (child == null) { + throw new IllegalStateException(getClass().getSimpleName() + " contains null " + + "child at index " + i + " when traversal in dispatchGetDisplayList," + + " the view may have been removed."); + } if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) { recreateChildDisplayList(child); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index f021bdfe478f..e10cc28d0745 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4345,6 +4345,7 @@ public final class ViewRootImpl implements ViewParent, handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions, mPendingTransaction, "view not visible"); + mHasPendingTransactions = false; } else if (cancelAndRedraw) { if (!mWasLastDrawCanceled) { logAndTrace("Canceling draw." @@ -4372,6 +4373,7 @@ public final class ViewRootImpl implements ViewParent, if (!performDraw(mActiveSurfaceSyncGroup)) { handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions, mPendingTransaction, mLastPerformDrawSkippedReason); + mHasPendingTransactions = false; } } mWasLastDrawCanceled = cancelAndRedraw; @@ -4388,7 +4390,14 @@ public final class ViewRootImpl implements ViewParent, mReportNextDraw = false; mLastReportNextDrawReason = null; mActiveSurfaceSyncGroup = null; - mHasPendingTransactions = false; + if (mHasPendingTransactions) { + // TODO: We shouldn't ever actually hit this, it means mPendingTransaction wasn't + // merged with a sync group or BLASTBufferQueue before making it to this point + // But better a one or two frame flicker than steady-state broken from dropping + // whatever is in this transaction + mPendingTransaction.apply(); + mHasPendingTransactions = false; + } mSyncBuffer = false; if (isInWMSRequestedSync()) { mWmsRequestSyncGroup.markSyncReady(); @@ -5305,6 +5314,7 @@ public final class ViewRootImpl implements ViewParent, private void registerCallbackForPendingTransactions() { Transaction t = new Transaction(); t.merge(mPendingTransaction); + mHasPendingTransactions = false; registerRtFrameCallback(new FrameDrawingCallback() { @Override @@ -5384,6 +5394,7 @@ public final class ViewRootImpl implements ViewParent, if (!usingAsyncReport && mHasPendingTransactions) { pendingTransaction = new Transaction(); pendingTransaction.merge(mPendingTransaction); + mHasPendingTransactions = false; } else { pendingTransaction = null; } @@ -9942,6 +9953,7 @@ public final class ViewRootImpl implements ViewParent, } handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions, mPendingTransaction, "shutting down VRI"); + mHasPendingTransactions = false; WindowManagerGlobal.getInstance().doRemoveView(this); } @@ -12601,6 +12613,7 @@ public final class ViewRootImpl implements ViewParent, if (mHasPendingTransactions) { t = new Transaction(); t.merge(mPendingTransaction); + mHasPendingTransactions = false; } else { t = null; } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index a87e5c8e1b56..2b7cf427a562 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -1774,7 +1774,8 @@ public final class AccessibilityManager { } /** - * Notifies that the accessibility button in the system's navigation area has been clicked + * Notifies that the accessibility button in the system's navigation area has been clicked, + * or a gesture shortcut input has been performed. * * @param displayId The logical display id. * @hide @@ -1785,7 +1786,8 @@ public final class AccessibilityManager { } /** - * Perform the accessibility button for the given target which is assigned to the button. + * Perform the accessibility button or gesture + * for the given target which is assigned to the button. * * @param displayId displayId The logical display id. * @param targetName The flattened {@link ComponentName} string or the class name of a system @@ -1810,6 +1812,31 @@ public final class AccessibilityManager { } /** + * Notifies that a shortcut was long-clicked. + * This displays the dialog used to select which target the given shortcut will use, + * from its list of targets. + * The current shortcut type is determined by the current navigation mode. + * + * @param displayId The id of the display to show the dialog on. + * @hide + */ + @RequiresPermission(Manifest.permission.STATUS_BAR_SERVICE) + public void notifyAccessibilityButtonLongClicked(int displayId) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.notifyAccessibilityButtonLongClicked(displayId); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while dispatching accessibility button long click. ", re); + } + } + + /** * Notifies that the visibility of the accessibility button in the system's navigation area * has changed. * diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 2de3ce8532e3..e04fa1537d54 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -88,6 +88,8 @@ interface IAccessibilityManager { @EnforcePermission("STATUS_BAR_SERVICE") void notifyAccessibilityButtonClicked(int displayId, String targetName); + @EnforcePermission("STATUS_BAR_SERVICE") + void notifyAccessibilityButtonLongClicked(int displayId); @EnforcePermission("STATUS_BAR_SERVICE") void notifyAccessibilityButtonVisibilityChanged(boolean available); diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 8501474b70a6..4c5802ccfcf5 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -137,9 +137,13 @@ public final class WebViewDelegate { */ @Deprecated public void detachDrawGlFunctor(View containerView, long nativeDrawGLFunctor) { - ViewRootImpl viewRootImpl = containerView.getViewRootImpl(); - if (nativeDrawGLFunctor != 0 && viewRootImpl != null) { - viewRootImpl.detachFunctor(nativeDrawGLFunctor); + if (Flags.mainlineApis()) { + throw new UnsupportedOperationException(); + } else { + ViewRootImpl viewRootImpl = containerView.getViewRootImpl(); + if (nativeDrawGLFunctor != 0 && viewRootImpl != null) { + viewRootImpl.detachFunctor(nativeDrawGLFunctor); + } } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index eb3581717637..a395c1a05744 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1447,6 +1447,11 @@ public class RemoteViews implements Parcelable, Filter { } @Override + public void onNullBinding(ComponentName name) { + context.unbindService(this); + } + + @Override public void onServiceDisconnected(ComponentName componentName) { } }); diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 2f28a8704cd3..118edc29f378 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -241,6 +241,11 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback } @Override + public void onNullBinding(ComponentName name) { + enqueueDeferredUnbindServiceMessage(); + } + + @Override public void handleMessage(Message msg) { RemoteViewsAdapter adapter = mAdapter.get(); diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index b5bf529fadbd..511c832a4876 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -16,6 +16,7 @@ package android.widget; +import static android.view.flags.Flags.enableTouchScrollFeedback; import static android.view.flags.Flags.viewVelocityApi; import android.annotation.ColorInt; @@ -846,6 +847,8 @@ public class ScrollView extends FrameLayout { deltaY += mTouchSlop; } } + boolean hitTopLimit = false; + boolean hitBottomLimit = false; if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; @@ -889,12 +892,14 @@ public class ScrollView extends FrameLayout { if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } + hitTopLimit = true; } else if (pulledToY > range) { mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(), 1.f - displacement); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } + hitBottomLimit = true; } if (shouldDisplayEdgeEffects() && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { @@ -902,6 +907,20 @@ public class ScrollView extends FrameLayout { } } } + + // TODO: b/360198915 - Add unit tests. + if (enableTouchScrollFeedback()) { + if (hitTopLimit || hitBottomLimit) { + initHapticScrollFeedbackProviderIfNotExists(); + mHapticScrollFeedbackProvider.onScrollLimit(vtev.getDeviceId(), + vtev.getSource(), MotionEvent.AXIS_Y, + /* isStart= */ hitTopLimit); + } else if (Math.abs(deltaY) != 0) { + initHapticScrollFeedbackProviderIfNotExists(); + mHapticScrollFeedbackProvider.onScrollProgress(vtev.getDeviceId(), + vtev.getSource(), MotionEvent.AXIS_Y, deltaY); + } + } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index a4b28adae4a1..ef941da0e32d 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -1659,11 +1659,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (!hasUseBoundForWidthValue) { - if (CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH)) { - mUseBoundsForWidth = Flags.useBoundsForWidth(); - } else { - mUseBoundsForWidth = false; - } + mUseBoundsForWidth = CompatChanges.isChangeEnabled(USE_BOUNDS_FOR_WIDTH); } // TODO(b/179693024): Use a ChangeId instead. diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig new file mode 100644 index 000000000000..f0ed83be8f1e --- /dev/null +++ b/core/java/android/widget/flags/flags.aconfig @@ -0,0 +1,11 @@ +package: "android.widget.flags" +container: "system" +flag { + name: "enable_fading_view_group" + namespace: "system_performance" + description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches" + bug: "348515581" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 4cc0d8a77a2b..c316800108bd 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -69,6 +69,23 @@ public class TaskFragmentOrganizer extends WindowOrganizer { public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type"; /** + * Key to bundle {@link TaskFragmentInfo}s from the system in + * {@link #registerOrganizer(boolean, Bundle)} + * + * @hide + */ + public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info"; + + /** + * Key to bundle {@link TaskFragmentParentInfo} from the system in + * {@link #registerOrganizer(boolean, Bundle)} + * + * @hide + */ + public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO = + "key_restore_task_fragment_parent_info"; + + /** * No change set. */ @WindowManager.TransitionType diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java index 01cbb5514669..81d8adfce2ae 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityButtonChooserActivity.java @@ -20,7 +20,6 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; -import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logAccessibilityButtonLongPressStatus; @@ -44,6 +43,8 @@ import java.util.List; * Activity used to display and persist a service or feature target for the Accessibility button. */ public class AccessibilityButtonChooserActivity extends Activity { + public static final String EXTRA_TYPE_TO_CHOOSE = "TYPE"; + private final List<AccessibilityTarget> mTargets = new ArrayList<>(); @Override @@ -67,8 +68,8 @@ public class AccessibilityButtonChooserActivity extends Activity { NAV_BAR_MODE_GESTURAL == getResources().getInteger( com.android.internal.R.integer.config_navBarInteractionMode); - final int targetType = (isGestureNavigateEnabled - && android.provider.Flags.a11yStandaloneGestureEnabled()) ? GESTURE : SOFTWARE; + final int targetType = android.provider.Flags.a11yStandaloneGestureEnabled() + ? getIntent().getIntExtra(EXTRA_TYPE_TO_CHOOSE, SOFTWARE) : SOFTWARE; if (isGestureNavigateEnabled) { final TextView promptPrologue = findViewById(R.id.accessibility_button_prompt_prologue); diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java index 07fa679a428a..dfb2884044f5 100644 --- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java @@ -18,6 +18,10 @@ package com.android.internal.os; import android.os.Parcel; import android.os.Parcelable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; +import android.ravenwood.annotation.RavenwoodReplace; import com.android.internal.util.Preconditions; @@ -55,18 +59,15 @@ import java.util.concurrent.atomic.AtomicReference; * * @hide */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.LongArrayMultiStateCounter_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("LongArrayMultiStateCounter_host") public final class LongArrayMultiStateCounter implements Parcelable { /** * Container for a native equivalent of a long[]. */ - @android.ravenwood.annotation.RavenwoodKeepWholeClass - @android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution" - + ".LongArrayMultiStateCounter_host$LongArrayContainer_host") + @RavenwoodKeepWholeClass + @RavenwoodRedirectionClass("LongArrayContainer_host") public static class LongArrayContainer { private static NativeAllocationRegistry sRegistry; @@ -81,7 +82,7 @@ public final class LongArrayMultiStateCounter implements Parcelable { registerNativeAllocation(); } - @android.ravenwood.annotation.RavenwoodReplace + @RavenwoodReplace private void registerNativeAllocation() { if (sRegistry == null) { synchronized (LongArrayMultiStateCounter.class) { @@ -140,18 +141,23 @@ public final class LongArrayMultiStateCounter implements Parcelable { } @CriticalNative + @RavenwoodRedirect private static native long native_init(int length); @CriticalNative + @RavenwoodRedirect private static native long native_getReleaseFunc(); @FastNative + @RavenwoodRedirect private static native void native_setValues(long nativeObject, long[] array); @FastNative + @RavenwoodRedirect private static native void native_getValues(long nativeObject, long[] array); @FastNative + @RavenwoodRedirect private static native boolean native_combineValues(long nativeObject, long[] array, int[] indexMap); } @@ -175,7 +181,7 @@ public final class LongArrayMultiStateCounter implements Parcelable { registerNativeAllocation(); } - @android.ravenwood.annotation.RavenwoodReplace + @RavenwoodReplace private void registerNativeAllocation() { if (sRegistry == null) { synchronized (LongArrayMultiStateCounter.class) { @@ -374,57 +380,73 @@ public final class LongArrayMultiStateCounter implements Parcelable { @CriticalNative + @RavenwoodRedirect private static native long native_init(int stateCount, int arrayLength); @CriticalNative + @RavenwoodRedirect private static native long native_getReleaseFunc(); @CriticalNative + @RavenwoodRedirect private static native void native_setEnabled(long nativeObject, boolean enabled, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_setState(long nativeObject, int state, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_copyStatesFrom(long nativeObjectTarget, long nativeObjectSource); @CriticalNative + @RavenwoodRedirect private static native void native_setValues(long nativeObject, int state, long longArrayContainerNativeObject); @CriticalNative + @RavenwoodRedirect private static native void native_updateValues(long nativeObject, long longArrayContainerNativeObject, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_incrementValues(long nativeObject, long longArrayContainerNativeObject, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_addCounts(long nativeObject, long longArrayContainerNativeObject); @CriticalNative + @RavenwoodRedirect private static native void native_reset(long nativeObject); @CriticalNative + @RavenwoodRedirect private static native void native_getCounts(long nativeObject, long longArrayContainerNativeObject, int state); @FastNative + @RavenwoodRedirect private static native String native_toString(long nativeObject); @FastNative + @RavenwoodRedirect private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags); @FastNative + @RavenwoodRedirect private static native long native_initFromParcel(Parcel parcel); @CriticalNative + @RavenwoodRedirect private static native int native_getStateCount(long nativeObject); @CriticalNative + @RavenwoodRedirect private static native int native_getArrayLength(long nativeObject); } diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java index e5662c7d5145..c386a86f5906 100644 --- a/core/java/com/android/internal/os/LongMultiStateCounter.java +++ b/core/java/com/android/internal/os/LongMultiStateCounter.java @@ -18,6 +18,10 @@ package com.android.internal.os; import android.os.Parcel; import android.os.Parcelable; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; +import android.ravenwood.annotation.RavenwoodReplace; import com.android.internal.util.Preconditions; @@ -55,9 +59,8 @@ import libcore.util.NativeAllocationRegistry; * * @hide */ -@android.ravenwood.annotation.RavenwoodKeepWholeClass -@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.LongMultiStateCounter_host") +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("LongMultiStateCounter_host") public final class LongMultiStateCounter implements Parcelable { private static NativeAllocationRegistry sRegistry; @@ -82,7 +85,7 @@ public final class LongMultiStateCounter implements Parcelable { mStateCount = native_getStateCount(mNativeObject); } - @android.ravenwood.annotation.RavenwoodReplace + @RavenwoodReplace private void registerNativeAllocation() { if (sRegistry == null) { synchronized (LongMultiStateCounter.class) { @@ -210,43 +213,56 @@ public final class LongMultiStateCounter implements Parcelable { @CriticalNative + @RavenwoodRedirect private static native long native_init(int stateCount); @CriticalNative + @RavenwoodRedirect private static native long native_getReleaseFunc(); @CriticalNative + @RavenwoodRedirect private static native void native_setEnabled(long nativeObject, boolean enabled, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_setState(long nativeObject, int state, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native long native_updateValue(long nativeObject, long value, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_incrementValue(long nativeObject, long increment, long timestampMs); @CriticalNative + @RavenwoodRedirect private static native void native_addCount(long nativeObject, long count); @CriticalNative + @RavenwoodRedirect private static native void native_reset(long nativeObject); @CriticalNative + @RavenwoodRedirect private static native long native_getCount(long nativeObject, int state); @FastNative + @RavenwoodRedirect private static native String native_toString(long nativeObject); @FastNative + @RavenwoodRedirect private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags); @FastNative + @RavenwoodRedirect private static native long native_initFromParcel(Parcel parcel); @CriticalNative + @RavenwoodRedirect private static native int native_getStateCount(long nativeObject); } diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index cdac09796137..1709ca78af4b 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -404,6 +404,17 @@ public class RuntimeInit { } public static void redirectLogStreams$ravenwood() { + if (sOut$ravenwood != null && sErr$ravenwood != null) { + return; // Already initialized. + } + + // Make sure the Log class is loaded and the JNI methods are hooked up, + // before redirecting System.out/err. + // Otherwise, because ClassLoadHook tries to write to System.out, this would cause + // a circular initialization problem and would cause a UnsatisfiedLinkError + // on the JNI methods. + Log.isLoggable("X", Log.VERBOSE); + if (sOut$ravenwood == null) { sOut$ravenwood = System.out; System.setOut(new AndroidPrintStream(Log.INFO, "System.out")); diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 58818f35de22..4708be8108c2 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -1144,7 +1144,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind mDrawLegacyNavigationBarBackground = ((requestedVisibleTypes | mLastForceConsumingTypes) & WindowInsets.Type.navigationBars()) != 0 - && (mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0; + && (mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0 + && navBarSize > 0; if (oldDrawLegacy != mDrawLegacyNavigationBarBackground) { mDrawLegacyNavigationBarBackgroundHandled = mWindow.onDrawLegacyNavigationBarBackgroundChanged( diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 1fd933f29789..e440dc9053fd 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -34,7 +34,6 @@ import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Gro import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL; -import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.INTERNED_DATA; import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.PROTOLOG_MESSAGE; @@ -72,7 +71,6 @@ import com.android.internal.protolog.common.LogLevel; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -103,6 +101,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private final ProtoLogDataSource mDataSource; @Nullable private final ProtoLogViewerConfigReader mViewerConfigReader; + @Deprecated @Nullable private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; @NonNull @@ -148,6 +147,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto cacheUpdater, groups); } + @Deprecated @VisibleForTesting public PerfettoProtoLogImpl( @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, @@ -160,6 +160,18 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto groups, dataSourceBuilder, configurationService); } + @VisibleForTesting + public PerfettoProtoLogImpl( + @Nullable String viewerConfigFilePath, + @Nullable ProtoLogViewerConfigReader viewerConfigReader, + @NonNull Runnable cacheUpdater, + @NonNull IProtoLogGroup[] groups, + @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, + @NonNull ProtoLogConfigurationService configurationService) { + this(viewerConfigFilePath, null, viewerConfigReader, cacheUpdater, + groups, dataSourceBuilder, configurationService); + } + private PerfettoProtoLogImpl( @Nullable String viewerConfigFilePath, @Nullable ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, @@ -449,6 +461,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto Log.d(LOG_TAG, "Finished onTracingFlush"); } + @Deprecated private void dumpViewerConfig() { if (mViewerConfigInputStreamProvider == null) { // No viewer config available @@ -457,103 +470,29 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto Log.d(LOG_TAG, "Dumping viewer config to trace"); - mDataSource.trace(ctx -> { - try { - ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); - - final ProtoOutputStream os = ctx.newTracePacket(); - - os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); - - final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - if (pis.getFieldNumber() == (int) MESSAGES) { - writeViewerConfigMessage(pis, os); - } - - if (pis.getFieldNumber() == (int) GROUPS) { - writeViewerConfigGroup(pis, os); - } - } - - os.end(outProtologViewerConfigToken); - } catch (IOException e) { - Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e); - } - }); + Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider); Log.d(LOG_TAG, "Dumped viewer config to trace"); } - private static void writeViewerConfigGroup( - ProtoInputStream pis, ProtoOutputStream os) throws IOException { - final long inGroupToken = pis.start(GROUPS); - final long outGroupToken = os.start(GROUPS); - - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - switch (pis.getFieldNumber()) { - case (int) ID: - int id = pis.readInt(ID); - os.write(ID, id); - break; - case (int) NAME: - String name = pis.readString(NAME); - os.write(NAME, name); - break; - case (int) TAG: - String tag = pis.readString(TAG); - os.write(TAG, tag); - break; - default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); - } - } - - pis.end(inGroupToken); - os.end(outGroupToken); - } - - private static void writeViewerConfigMessage( - ProtoInputStream pis, ProtoOutputStream os) throws IOException { - final long inMessageToken = pis.start(MESSAGES); - final long outMessagesToken = os.start(MESSAGES); - - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - switch (pis.getFieldNumber()) { - case (int) MessageData.MESSAGE_ID: - os.write(MessageData.MESSAGE_ID, - pis.readLong(MessageData.MESSAGE_ID)); - break; - case (int) MESSAGE: - os.write(MESSAGE, pis.readString(MESSAGE)); - break; - case (int) LEVEL: - os.write(LEVEL, pis.readInt(LEVEL)); - break; - case (int) GROUP_ID: - os.write(GROUP_ID, pis.readInt(GROUP_ID)); - break; - case (int) LOCATION: - os.write(LOCATION, pis.readString(LOCATION)); - break; - default: - throw new RuntimeException( - "Unexpected field id " + pis.getFieldNumber()); - } - } - - pis.end(inMessageToken); - os.end(outMessagesToken); - } - private void logToLogcat(String tag, LogLevel level, Message message, @Nullable Object[] args) { String messageString; if (mViewerConfigReader == null) { messageString = message.getMessage(); + + if (messageString == null) { + Log.e(LOG_TAG, "Failed to decode message for logcat. " + + "Message not available without ViewerConfig to decode the hash."); + } } else { messageString = message.getMessage(mViewerConfigReader); + + if (messageString == null) { + Log.e(LOG_TAG, "Failed to decode message for logcat. " + + "Message hash either not available in viewerConfig file or " + + "not loaded into memory from file before decoding."); + } } if (messageString == null) { @@ -760,7 +699,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto os.write(MessageData.MESSAGE_ID, messageHash); os.write(MESSAGE, message); - os.write(LEVEL, level.ordinal()); + os.write(LEVEL, level.id); os.write(GROUP_ID, logGroup.getId()); os.end(messageConfigToken); @@ -954,8 +893,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto private static class Message { @Nullable private final Long mMessageHash; - @Nullable - private final Integer mMessageMask; + private final int mMessageMask; @Nullable private final String mMessageString; @@ -972,8 +910,7 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto this.mMessageString = messageString; } - @Nullable - private Integer getMessageMask() { + private int getMessageMask() { return mMessageMask; } diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java index d54a80b7c921..7031d694f09c 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationService.java @@ -26,8 +26,6 @@ import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Mes import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; -import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.PROTOLOG_VIEWER_CONFIG; -import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.TIMESTAMP; import android.annotation.NonNull; import android.annotation.Nullable; @@ -36,7 +34,6 @@ import android.content.Context; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.os.SystemClock; import android.tracing.perfetto.DataSourceParams; import android.tracing.perfetto.InitArguments; import android.tracing.perfetto.Producer; @@ -125,7 +122,8 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe this(ProtoLogDataSource::new, tracer); } - private ProtoLogConfigurationService( + @VisibleForTesting + public ProtoLogConfigurationService( @NonNull ProtoLogDataSourceBuilder dataSourceBuilder, @NonNull ViewerConfigFileTracer tracer) { mDataSource = dataSourceBuilder.build( @@ -374,32 +372,13 @@ public final class ProtoLogConfigurationService extends IProtoLogConfigurationSe private static void dumpTransitionTraceConfig(@NonNull ProtoLogDataSource dataSource, @NonNull String viewerConfigFilePath) { - dataSource.trace(ctx -> { - final ProtoInputStream pis; + Utils.dumpViewerConfig(dataSource, () -> { try { - pis = new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); + return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); } catch (FileNotFoundException e) { throw new RuntimeException( "Failed to load viewer config file " + viewerConfigFilePath, e); } - - try { - final ProtoOutputStream os = ctx.newTracePacket(); - - os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); - - final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); - while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { - switch (pis.getFieldNumber()) { - case (int) MESSAGES -> writeViewerConfigMessage(pis, os); - case (int) GROUPS -> writeViewerConfigGroup(pis, os); - } - } - - os.end(outProtologViewerConfigToken); - } catch (IOException e) { - Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e); - } }); } diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 1b2f5f7ccf2f..0afb135ac6d9 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -283,10 +283,24 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, public static class Instance extends DataSourceInstance { public interface TracingInstanceStartCallback { + /** + * Execute the tracing instance's onStart callback. + * @param instanceIdx The index of the tracing instance we are executing the callback + * for. + * @param config The protolog configuration for the tracing instance we are executing + * the callback for. + */ void run(int instanceIdx, @NonNull ProtoLogConfig config); } public interface TracingInstanceStopCallback { + /** + * Execute the tracing instance's onStop callback. + * @param instanceIdx The index of the tracing instance we are executing the callback + * for. + * @param config The protolog configuration for the tracing instance we are executing + * the callback for. + */ void run(int instanceIdx, @NonNull ProtoLogConfig config); } diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index da6d8cff6890..7bdcf2d14b19 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -23,6 +23,7 @@ import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.LO import static com.android.internal.protolog.common.ProtoLogToolInjected.Value.VIEWER_CONFIG_PATH; import android.annotation.Nullable; +import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.IProtoLog; @@ -37,6 +38,8 @@ import java.util.TreeMap; * A service for the ProtoLog logging system. */ public class ProtoLogImpl { + private static final String LOG_TAG = "ProtoLogImpl"; + private static IProtoLog sServiceInstance = null; @ProtoLogToolInjected(VIEWER_CONFIG_PATH) @@ -97,6 +100,9 @@ public class ProtoLogImpl { */ public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { + Log.i(LOG_TAG, "Setting up " + ProtoLogImpl.class.getSimpleName() + " with " + + "viewerConfigPath = " + sViewerConfigPath); + final var groups = sLogGroups.values().toArray(new IProtoLogGroup[0]); if (android.tracing.Flags.perfettoProtologTracing()) { @@ -105,6 +111,9 @@ public class ProtoLogImpl { // TODO(b/353530422): Remove - temporary fix to unblock b/352290057 // In some tests the viewer config file might not exist in which we don't // want to provide config path to the user + Log.w(LOG_TAG, "Failed to find viewerConfigFile when setting up " + + ProtoLogImpl.class.getSimpleName() + ". " + + "Setting up without a viewer config instead..."); sServiceInstance = new PerfettoProtoLogImpl(sCacheUpdater, groups); } else { sServiceInstance = diff --git a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java index 6c8996e610dc..0a80e006d5bc 100644 --- a/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java +++ b/core/java/com/android/internal/protolog/ProtoLogViewerConfigReader.java @@ -43,6 +43,13 @@ public class ProtoLogViewerConfigReader { return mLogMessageMap.get(messageHash); } + /** + * Load the viewer configs for the target groups into memory. + * Only viewer configs loaded into memory can be required. So this must be called for all groups + * we want to query before we query their viewer config. + * + * @param groups Groups to load the viewer configs from file into memory. + */ public synchronized void loadViewerConfig(@NonNull String[] groups) { loadViewerConfig(groups, (message) -> {}); } diff --git a/core/java/com/android/internal/protolog/Utils.java b/core/java/com/android/internal/protolog/Utils.java new file mode 100644 index 000000000000..1e6ba309c046 --- /dev/null +++ b/core/java/com/android/internal/protolog/Utils.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open 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.protolog; + +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.TAG; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LEVEL; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.LOCATION; +import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; +import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.PROTOLOG_VIEWER_CONFIG; +import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.TIMESTAMP; + +import android.annotation.NonNull; +import android.internal.perfetto.protos.Protolog; +import android.os.SystemClock; +import android.util.Log; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import java.io.IOException; + +public class Utils { + private static final String LOG_TAG = "ProtoLogUtils"; + + /** + * Dump the viewer config provided by the input stream to the target datasource. + * @param dataSource The datasource to dump the ProtoLog viewer config to. + * @param viewerConfigInputStreamProvider The InputStream that provided the proto viewer config. + */ + public static void dumpViewerConfig(@NonNull ProtoLogDataSource dataSource, + @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { + dataSource.trace(ctx -> { + try { + ProtoInputStream pis = viewerConfigInputStreamProvider.getInputStream(); + + final ProtoOutputStream os = ctx.newTracePacket(); + + os.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos()); + + final long outProtologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (pis.getFieldNumber() == (int) MESSAGES) { + writeViewerConfigMessage(pis, os); + } + + if (pis.getFieldNumber() == (int) GROUPS) { + writeViewerConfigGroup(pis, os); + } + } + + os.end(outProtologViewerConfigToken); + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump to datasource", e); + } + }); + } + + private static void writeViewerConfigGroup( + @NonNull ProtoInputStream pis, @NonNull ProtoOutputStream os) throws IOException { + final long inGroupToken = pis.start(GROUPS); + final long outGroupToken = os.start(GROUPS); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) ID: + int id = pis.readInt(ID); + os.write(ID, id); + break; + case (int) NAME: + String name = pis.readString(NAME); + os.write(NAME, name); + break; + case (int) TAG: + String tag = pis.readString(TAG); + os.write(TAG, tag); + break; + default: + throw new RuntimeException( + "Unexpected field id " + pis.getFieldNumber()); + } + } + + pis.end(inGroupToken); + os.end(outGroupToken); + } + + private static void writeViewerConfigMessage( + ProtoInputStream pis, ProtoOutputStream os) throws IOException { + final long inMessageToken = pis.start(MESSAGES); + final long outMessagesToken = os.start(MESSAGES); + + while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (pis.getFieldNumber()) { + case (int) Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID: + os.write(Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID, + pis.readLong(Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID)); + break; + case (int) MESSAGE: + os.write(MESSAGE, pis.readString(MESSAGE)); + break; + case (int) LEVEL: + os.write(LEVEL, pis.readInt(LEVEL)); + break; + case (int) GROUP_ID: + os.write(GROUP_ID, pis.readInt(GROUP_ID)); + break; + case (int) LOCATION: + os.write(LOCATION, pis.readString(LOCATION)); + break; + default: + throw new RuntimeException( + "Unexpected field id " + pis.getFieldNumber()); + } + } + + pis.end(inMessageToken); + os.end(outMessagesToken); + } + +} diff --git a/core/java/com/android/internal/protolog/common/LogLevel.java b/core/java/com/android/internal/protolog/common/LogLevel.java index 16c34e1f333e..b5541ae81c2d 100644 --- a/core/java/com/android/internal/protolog/common/LogLevel.java +++ b/core/java/com/android/internal/protolog/common/LogLevel.java @@ -17,10 +17,18 @@ package com.android.internal.protolog.common; public enum LogLevel { - DEBUG("d"), VERBOSE("v"), INFO("i"), WARN("w"), ERROR("e"), WTF("wtf"); + DEBUG("d", 1), + VERBOSE("v", 2), + INFO("i", 3), + WARN("w", 4), + ERROR("e", 5), + WTF("wtf", 6); public final String shortCode; - LogLevel(String shortCode) { + public final int id; + + LogLevel(String shortCode, int id) { this.shortCode = shortCode; + this.id = id; } } diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java index 319efe04da8c..30b160ab161b 100644 --- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java +++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java @@ -16,15 +16,15 @@ package com.android.internal.ravenwood; import android.ravenwood.annotation.RavenwoodKeepWholeClass; -import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; import android.ravenwood.annotation.RavenwoodReplace; /** * Class to interact with the Ravenwood environment. */ @RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.RavenwoodEnvironment_host") +@RavenwoodRedirectionClass("RavenwoodEnvironment_host") public final class RavenwoodEnvironment { public static final String TAG = "RavenwoodEnvironment"; @@ -40,7 +40,7 @@ public final class RavenwoodEnvironment { ensureRavenwoodInitialized(); } - private static RuntimeException notSupportedOnDevice() { + public static RuntimeException notSupportedOnDevice() { return new UnsupportedOperationException("This method can only be used on Ravenwood"); } @@ -56,14 +56,10 @@ public final class RavenwoodEnvironment { * * No-op if called on the device side. */ - @RavenwoodReplace + @RavenwoodRedirect public static void ensureRavenwoodInitialized() { } - private static void ensureRavenwoodInitialized$ravenwood() { - nativeEnsureRavenwoodInitialized(); - } - /** * USE IT SPARINGLY! Returns true if it's running on Ravenwood, hostside test environment. * @@ -89,15 +85,11 @@ public final class RavenwoodEnvironment { * Get the object back from the address obtained from * {@link dalvik.system.VMRuntime#addressOf(Object)}. */ - @RavenwoodReplace + @RavenwoodRedirect public <T> T fromAddress(long address) { throw notSupportedOnDevice(); } - private <T> T fromAddress$ravenwood(long address) { - return nativeFromAddress(address); - } - /** * See {@link Workaround}. It's only usable on Ravenwood. */ @@ -113,20 +105,11 @@ public final class RavenwoodEnvironment { /** * @return the "ravenwood-runtime" directory. */ - @RavenwoodReplace + @RavenwoodRedirect public String getRavenwoodRuntimePath() { throw notSupportedOnDevice(); } - private String getRavenwoodRuntimePath$ravenwood() { - return nativeGetRavenwoodRuntimePath(); - } - - // Private native methods that are actually substituted on Ravenwood - private native <T> T nativeFromAddress(long address); - private native String nativeGetRavenwoodRuntimePath(); - private static native void nativeEnsureRavenwoodInitialized(); - /** * A set of APIs used to work around missing features on Ravenwood. Ideally, this class should * be empty, and all its APIs should be able to be implemented properly. diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index 11c220b14bcc..0ec55f958f38 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -120,6 +120,7 @@ public class LockPatternView extends View { private static final String TAG = "LockPatternView"; private OnPatternListener mOnPatternListener; + private ExternalHapticsPlayer mExternalHapticsPlayer; @UnsupportedAppUsage private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9); @@ -317,6 +318,13 @@ public class LockPatternView extends View { void onPatternDetected(List<Cell> pattern); } + /** An external haptics player for pattern updates. */ + public interface ExternalHapticsPlayer{ + + /** Perform haptic feedback when a cell is added to the pattern. */ + void performCellAddedFeedback(); + } + public LockPatternView(Context context) { this(context, null); } @@ -461,6 +469,15 @@ public class LockPatternView extends View { } /** + * Set the external haptics player for feedback on pattern detection. + * @param player The external player. + */ + @UnsupportedAppUsage + public void setExternalHapticsPlayer(ExternalHapticsPlayer player) { + mExternalHapticsPlayer = player; + } + + /** * Set the pattern explicitely (rather than waiting for the user to input * a pattern). * @param displayMode How to display the pattern. @@ -847,6 +864,16 @@ public class LockPatternView extends View { return null; } + @Override + public boolean performHapticFeedback(int feedbackConstant, int flags) { + if (mExternalHapticsPlayer != null) { + mExternalHapticsPlayer.performCellAddedFeedback(); + return true; + } else { + return super.performHapticFeedback(feedbackConstant, flags); + } + } + private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; mPattern.add(newCell); diff --git a/core/java/com/android/internal/widget/ViewGroupFader.java b/core/java/com/android/internal/widget/ViewGroupFader.java index b54023a3382e..21206c244c8f 100644 --- a/core/java/com/android/internal/widget/ViewGroupFader.java +++ b/core/java/com/android/internal/widget/ViewGroupFader.java @@ -16,12 +16,14 @@ package com.android.internal.widget; +import android.content.res.Resources; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.BaseInterpolator; import android.view.animation.PathInterpolator; +import android.widget.flags.Flags; /** * This class is ported from @@ -36,7 +38,7 @@ import android.view.animation.PathInterpolator; * height of the child. When not in the top or bottom regions, children have their default alpha and * scale. */ -class ViewGroupFader { +public class ViewGroupFader { private static final float SCALE_LOWER_BOUND = 0.7f; private float mScaleLowerBound = SCALE_LOWER_BOUND; @@ -68,7 +70,7 @@ class ViewGroupFader { private BaseInterpolator mBottomInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f); /** Callback which is called when attempting to fade a view. */ - interface AnimationCallback { + public interface AnimationCallback { boolean shouldFadeFromTop(View view); boolean shouldFadeFromBottom(View view); @@ -82,7 +84,7 @@ class ViewGroupFader { * of the current position. */ // TODO(b/182846214): Clean up the interface design to avoid exposing too much details to users. - interface ChildViewBoundsProvider { + public interface ChildViewBoundsProvider { /** * Provide the bounds of the child view. * @@ -168,7 +170,7 @@ class ViewGroupFader { } } - ViewGroupFader( + public ViewGroupFader( ViewGroup parent, AnimationCallback callback, ChildViewBoundsProvider childViewBoundsProvider) { @@ -212,7 +214,7 @@ class ViewGroupFader { this.mContainerBoundsProvider = boundsProvider; } - void updateFade() { + public void updateFade() { mContainerBoundsProvider.provideBounds(mParent, mContainerBounds); mTopBoundPixels = mContainerBounds.height() * mChainedBoundsTop; mBottomBoundPixels = mContainerBounds.height() * mChainedBoundsBottom; @@ -221,13 +223,20 @@ class ViewGroupFader { } /** For each list element, calculate and adjust the scale and alpha based on its position */ - private void updateListElementFades(ViewGroup parent, boolean shouldFade) { + public void updateListElementFades(ViewGroup parent, boolean shouldFade) { for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); if (child.getVisibility() != View.VISIBLE) { continue; } + if (Flags.enableFadingViewGroup() && Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_enableViewGroupScalingFading)) { + if (child instanceof ViewGroup) { + updateListElementFades((ViewGroup) child, true); + } + } + if (shouldFade) { fadeElement(parent, child); } @@ -312,4 +321,4 @@ class ViewGroupFader { private static float lerp(float min, float max, float fraction) { return min + (max - min) * fraction; } -} +}
\ No newline at end of file diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 584ebaa221fc..dec724b6a7ff 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -90,7 +90,7 @@ void recycleJavaParcelObject(JNIEnv* env, jobject parcelObj) env->CallVoidMethod(parcelObj, gParcelOffsets.recycle); } -static void android_os_Parcel_markSensitive(jlong nativePtr) +static void android_os_Parcel_markSensitive(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel) { @@ -116,30 +116,30 @@ static void android_os_Parcel_markForBinder(JNIEnv* env, jclass clazz, jlong nat } } -static jboolean android_os_Parcel_isForRpc(jlong nativePtr) { +static jboolean android_os_Parcel_isForRpc(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->isForRpc() : false; } -static jint android_os_Parcel_dataSize(jlong nativePtr) +static jint android_os_Parcel_dataSize(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataSize() : 0; } -static jint android_os_Parcel_dataAvail(jlong nativePtr) +static jint android_os_Parcel_dataAvail(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataAvail() : 0; } -static jint android_os_Parcel_dataPosition(jlong nativePtr) +static jint android_os_Parcel_dataPosition(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataPosition() : 0; } -static jint android_os_Parcel_dataCapacity(jlong nativePtr) +static jint android_os_Parcel_dataCapacity(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return parcel ? parcel->dataCapacity() : 0; @@ -156,7 +156,7 @@ static void android_os_Parcel_setDataSize(JNIEnv* env, jclass clazz, jlong nativ } } -static void android_os_Parcel_setDataPosition(jlong nativePtr, jint pos) +static void android_os_Parcel_setDataPosition(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr, jint pos) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -175,7 +175,8 @@ static void android_os_Parcel_setDataCapacity(JNIEnv* env, jclass clazz, jlong n } } -static jboolean android_os_Parcel_pushAllowFds(jlong nativePtr, jboolean allowFds) +static jboolean android_os_Parcel_pushAllowFds(CRITICAL_JNI_PARAMS_COMMA + jlong nativePtr, jboolean allowFds) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); jboolean ret = JNI_TRUE; @@ -185,7 +186,8 @@ static jboolean android_os_Parcel_pushAllowFds(jlong nativePtr, jboolean allowFd return ret; } -static void android_os_Parcel_restoreAllowFds(jlong nativePtr, jboolean lastValue) +static void android_os_Parcel_restoreAllowFds(CRITICAL_JNI_PARAMS_COMMA + jlong nativePtr, jboolean lastValue) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -259,22 +261,22 @@ static void android_os_Parcel_writeBlob(JNIEnv* env, jclass clazz, jlong nativeP blob.release(); } -static int android_os_Parcel_writeInt(jlong nativePtr, jint val) { +static int android_os_Parcel_writeInt(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr, jint val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return (parcel != NULL) ? parcel->writeInt32(val) : OK; } -static int android_os_Parcel_writeLong(jlong nativePtr, jlong val) { +static int android_os_Parcel_writeLong(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr, jlong val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return (parcel != NULL) ? parcel->writeInt64(val) : OK; } -static int android_os_Parcel_writeFloat(jlong nativePtr, jfloat val) { +static int android_os_Parcel_writeFloat(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr, jfloat val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return (parcel != NULL) ? parcel->writeFloat(val) : OK; } -static int android_os_Parcel_writeDouble(jlong nativePtr, jdouble val) { +static int android_os_Parcel_writeDouble(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr, jdouble val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); return (parcel != NULL) ? parcel->writeDouble(val) : OK; } @@ -446,7 +448,7 @@ static jbyteArray android_os_Parcel_readBlob(JNIEnv* env, jclass clazz, jlong na return ret; } -static jint android_os_Parcel_readInt(jlong nativePtr) +static jint android_os_Parcel_readInt(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -455,7 +457,7 @@ static jint android_os_Parcel_readInt(jlong nativePtr) return 0; } -static jlong android_os_Parcel_readLong(jlong nativePtr) +static jlong android_os_Parcel_readLong(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -464,7 +466,7 @@ static jlong android_os_Parcel_readLong(jlong nativePtr) return 0; } -static jfloat android_os_Parcel_readFloat(jlong nativePtr) +static jfloat android_os_Parcel_readFloat(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -473,7 +475,7 @@ static jfloat android_os_Parcel_readFloat(jlong nativePtr) return 0; } -static jdouble android_os_Parcel_readDouble(jlong nativePtr) +static jdouble android_os_Parcel_readDouble(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -690,7 +692,7 @@ static jboolean android_os_Parcel_hasBindersInRange(JNIEnv* env, jclass clazz, j return JNI_FALSE; } -static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr) +static jboolean android_os_Parcel_hasFileDescriptors(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { jboolean ret = JNI_FALSE; Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); @@ -807,7 +809,7 @@ static jlong android_os_Parcel_getGlobalAllocCount(JNIEnv* env, jclass clazz) return Parcel::getGlobalAllocCount(); } -static jlong android_os_Parcel_getOpenAshmemSize(jlong nativePtr) +static jlong android_os_Parcel_getOpenAshmemSize(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -816,7 +818,7 @@ static jlong android_os_Parcel_getOpenAshmemSize(jlong nativePtr) return 0; } -static jint android_os_Parcel_readCallingWorkSourceUid(jlong nativePtr) +static jint android_os_Parcel_readCallingWorkSourceUid(CRITICAL_JNI_PARAMS_COMMA jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -825,7 +827,8 @@ static jint android_os_Parcel_readCallingWorkSourceUid(jlong nativePtr) return IPCThreadState::kUnsetWorkSource; } -static jboolean android_os_Parcel_replaceCallingWorkSourceUid(jlong nativePtr, jint uid) +static jboolean android_os_Parcel_replaceCallingWorkSourceUid(CRITICAL_JNI_PARAMS_COMMA + jlong nativePtr, jint uid) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 921b77d61f4d..f2c70b5f41d4 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -1152,60 +1152,60 @@ void signalExceptionForError(JNIEnv* env, jobject obj, status_t err, // ---------------------------------------------------------------------------- -static jint android_os_Binder_getCallingPid() +static jint android_os_Binder_getCallingPid(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->getCallingPid(); } -static jint android_os_Binder_getCallingUid() +static jint android_os_Binder_getCallingUid(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->getCallingUid(); } -static jboolean android_os_Binder_isDirectlyHandlingTransactionNative() { +static jboolean android_os_Binder_isDirectlyHandlingTransactionNative(CRITICAL_JNI_PARAMS) { return getCurrentServingCall() == BinderCallType::BINDER; } -static jlong android_os_Binder_clearCallingIdentity() +static jlong android_os_Binder_clearCallingIdentity(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->clearCallingIdentity(); } -static void android_os_Binder_restoreCallingIdentity(jlong token) +static void android_os_Binder_restoreCallingIdentity(CRITICAL_JNI_PARAMS_COMMA jlong token) { IPCThreadState::self()->restoreCallingIdentity(token); } -static jboolean android_os_Binder_hasExplicitIdentity() { +static jboolean android_os_Binder_hasExplicitIdentity(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->hasExplicitIdentity(); } -static void android_os_Binder_setThreadStrictModePolicy(jint policyMask) +static void android_os_Binder_setThreadStrictModePolicy(CRITICAL_JNI_PARAMS_COMMA jint policyMask) { IPCThreadState::self()->setStrictModePolicy(policyMask); } -static jint android_os_Binder_getThreadStrictModePolicy() +static jint android_os_Binder_getThreadStrictModePolicy(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->getStrictModePolicy(); } -static jlong android_os_Binder_setCallingWorkSourceUid(jint workSource) +static jlong android_os_Binder_setCallingWorkSourceUid(CRITICAL_JNI_PARAMS_COMMA jint workSource) { return IPCThreadState::self()->setCallingWorkSourceUid(workSource); } -static jlong android_os_Binder_getCallingWorkSourceUid() +static jlong android_os_Binder_getCallingWorkSourceUid(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->getCallingWorkSourceUid(); } -static jlong android_os_Binder_clearCallingWorkSource() +static jlong android_os_Binder_clearCallingWorkSource(CRITICAL_JNI_PARAMS) { return IPCThreadState::self()->clearCallingWorkSource(); } -static void android_os_Binder_restoreCallingWorkSource(jlong token) +static void android_os_Binder_restoreCallingWorkSource(CRITICAL_JNI_PARAMS_COMMA jlong token) { IPCThreadState::self()->restoreCallingWorkSource(token); } diff --git a/core/jni/android_view_TunnelModeEnabledListener.cpp b/core/jni/android_view_TunnelModeEnabledListener.cpp index af7bae8c89dd..d9ab9571cfbe 100644 --- a/core/jni/android_view_TunnelModeEnabledListener.cpp +++ b/core/jni/android_view_TunnelModeEnabledListener.cpp @@ -88,20 +88,19 @@ void nativeDestroy(JNIEnv* env, jclass clazz, jlong ptr) { void nativeRegister(JNIEnv* env, jclass clazz, jlong ptr) { sp<TunnelModeEnabledListener> listener = reinterpret_cast<TunnelModeEnabledListener*>(ptr); - if (SurfaceComposerClient::addTunnelModeEnabledListener(listener) != OK) { - constexpr auto error_msg = "Couldn't addTunnelModeEnabledListener"; - ALOGE(error_msg); - jniThrowRuntimeException(env, error_msg); + status_t status = SurfaceComposerClient::addTunnelModeEnabledListener(listener); + if (status != OK) { + ALOGE("Couldn't addTunnelModeEnabledListener (%d)", status); + jniThrowRuntimeException(env, "Couldn't addTunnelModeEnabledListener"); } } void nativeUnregister(JNIEnv* env, jclass clazz, jlong ptr) { sp<TunnelModeEnabledListener> listener = reinterpret_cast<TunnelModeEnabledListener*>(ptr); - - if (SurfaceComposerClient::removeTunnelModeEnabledListener(listener) != OK) { - constexpr auto error_msg = "Couldn't removeTunnelModeEnabledListener"; - ALOGE(error_msg); - jniThrowRuntimeException(env, error_msg); + status_t status = SurfaceComposerClient::removeTunnelModeEnabledListener(listener); + if (status != OK) { + ALOGE("Couldn't removeTunnelModeEnabledListener (%d)", status); + jniThrowRuntimeException(env, "Couldn't removeTunnelModeEnabledListener"); } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a19b71c68deb..d35c66ed719e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4033,7 +4033,6 @@ <!-- Allows an application to manage policy related to block package uninstallation. <p>Protection level: internal|role <p>Intended for use by the DEVICE_POLICY_MANAGEMENT role only. - @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_BLOCK_UNINSTALL" android:protectionLevel="internal|role" /> @@ -4041,7 +4040,6 @@ <!-- Allows an application to manage policy related to camera toggle. <p>Protection level: internal|role <p>Intended for use by the DEVICE_POLICY_MANAGEMENT role only. - @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CAMERA_TOGGLE" android:protectionLevel="internal|role" /> @@ -4049,7 +4047,6 @@ <!-- Allows an application to manage policy related to microphone toggle. <p>Protection level: internal|role <p>Intended for use by the DEVICE_POLICY_MANAGEMENT role only. - @FlaggedApi("android.app.admin.flags.dedicated_device_control_api_enabled") --> <permission android:name="android.permission.MANAGE_DEVICE_POLICY_MICROPHONE_TOGGLE" android:protectionLevel="internal|role" /> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 52662149b23a..e28b6462bad7 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -96,4 +96,8 @@ <!-- True if the device supports system decorations on secondary displays. --> <bool name="config_supportsSystemDecorsOnSecondaryDisplays">false</bool> + + <!-- Whether to enable scaling and fading animation to scrollviews while scrolling. + P.S this is a change only intended for wear devices. --> + <bool name="config_enableViewGroupScalingFading">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 38aff7590a42..f6267f6174b6 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7121,4 +7121,8 @@ <!-- The maximum number of call log entries for each sim card that can be stored in the call log provider on the device. --> <integer name="config_maximumCallLogEntriesPerSim">500</integer> + + <!-- Whether to enable scaling and fading animation to scrollviews while scrolling. + P.S this is a change only intended for wear devices. --> + <bool name="config_enableViewGroupScalingFading">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 46938948b133..3c8c04e23087 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5601,4 +5601,6 @@ <!-- Fingerprint loe notification string --> <java-symbol type="string" name="fingerprint_loe_notification_msg" /> + + <java-symbol type="bool" name="config_enableViewGroupScalingFading"/> </resources> diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 10aed8d51d09..14292725506e 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -16,8 +16,6 @@ package android.graphics; -import static com.android.text.flags.Flags.FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -32,7 +30,6 @@ import android.graphics.fonts.FontFamily; import android.graphics.fonts.SystemFonts; import android.graphics.text.PositionedGlyphs; import android.graphics.text.TextRunShaper; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.FontConfig; @@ -931,7 +928,6 @@ public class TypefaceSystemFallbackTest { return String.format(xml, op, lang, font); } - @RequiresFlagsEnabled(FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK) @Test public void testBuildSystemFallback__Customization_locale_prepend() { final ArrayMap<String, Typeface> fontMap = new ArrayMap<>(); @@ -947,7 +943,6 @@ public class TypefaceSystemFallbackTest { assertB3emFontIsUsed(typeface); } - @RequiresFlagsEnabled(FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK) @Test public void testBuildSystemFallback__Customization_locale_replace() { final ArrayMap<String, Typeface> fontMap = new ArrayMap<>(); @@ -963,7 +958,6 @@ public class TypefaceSystemFallbackTest { assertB3emFontIsUsed(typeface); } - @RequiresFlagsEnabled(FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK) @Test public void testBuildSystemFallback__Customization_locale_append() { final ArrayMap<String, Typeface> fontMap = new ArrayMap<>(); @@ -979,7 +973,6 @@ public class TypefaceSystemFallbackTest { assertA3emFontIsUsed(typeface); } - @RequiresFlagsEnabled(FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK) @Test public void testBuildSystemFallback__Customization_locale_ScriptMismatch() { final ArrayMap<String, Typeface> fontMap = new ArrayMap<>(); @@ -995,7 +988,6 @@ public class TypefaceSystemFallbackTest { assertA3emFontIsUsed(typeface); } - @RequiresFlagsEnabled(FLAG_VENDOR_CUSTOM_LOCALE_FALLBACK) @Test public void testBuildSystemFallback__Customization_locale_SubscriptMatch() { final ArrayMap<String, Typeface> fontMap = new ArrayMap<>(); diff --git a/core/tests/coretests/src/android/os/BinderTest.java b/core/tests/coretests/src/android/os/BinderTest.java index 9767d677807d..90ec93e46418 100644 --- a/core/tests/coretests/src/android/os/BinderTest.java +++ b/core/tests/coretests/src/android/os/BinderTest.java @@ -24,18 +24,16 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.testng.Assert.assertThrows; -import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import com.android.internal.os.BinderInternal; - import org.junit.Rule; import org.junit.Test; -@IgnoreUnderRavenwood(blockedBy = WorkSource.class) public class BinderTest { private static final int UID = 100; @@ -89,6 +87,7 @@ public class BinderTest { @SmallTest @Test(expected = java.lang.SecurityException.class) + @DisabledOnRavenwood(blockedBy = ServiceManagerNative.class) public void testServiceManagerNativeSecurityException() throws RemoteException { // Find the service manager IServiceManager sServiceManager = ServiceManagerNative @@ -101,6 +100,7 @@ public class BinderTest { @SmallTest @Test(expected = java.lang.NullPointerException.class) + @DisabledOnRavenwood(blockedBy = ServiceManagerNative.class) public void testServiceManagerNativeNullptrException() throws RemoteException { // Find the service manager IServiceManager sServiceManager = ServiceManagerNative diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index ded6fc5de2e5..31e07524d777 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -25,7 +25,6 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import android.platform.test.annotations.DisabledOnRavenwood; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; import android.util.Log; @@ -131,7 +130,6 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class) public void testCreateFromParcel() throws Exception { boolean withFd; Parcel p; @@ -312,7 +310,7 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) + @DisabledOnRavenwood(reason = "Ravenwood tests run on the BCP") public void kindofEquals_lazyValuesAndDifferentClassLoaders_returnsFalse() { Parcelable p1 = new CustomParcelable(13, "Tiramisu"); Parcelable p2 = new CustomParcelable(13, "Tiramisu"); @@ -368,7 +366,6 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void readWriteLengthMismatch_logsWtf() throws Exception { mWtfHandler = Log.setWtfHandler((tag, e, system) -> { throw new RuntimeException(e); @@ -383,7 +380,7 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) + @DisabledOnRavenwood(reason = "Ravenwood tests run on the BCP") public void getParcelable_whenThrowingAndNotDefusing_throws() throws Exception { Bundle.setShouldDefuse(false); Bundle bundle = new Bundle(); @@ -396,7 +393,7 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) + @DisabledOnRavenwood(reason = "Ravenwood tests run on the BCP") public void getParcelable_whenThrowingAndDefusing_returnsNull() throws Exception { Bundle.setShouldDefuse(true); Bundle bundle = new Bundle(); @@ -412,7 +409,7 @@ public class BundleTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) + @DisabledOnRavenwood(reason = "Ravenwood tests run on the BCP") public void getParcelable_whenThrowingAndDefusing_leavesElement() throws Exception { Bundle.setShouldDefuse(true); Bundle bundle = new Bundle(); @@ -447,7 +444,6 @@ public class BundleTest { } @Test - @DisabledOnRavenwood(blockedBy = Parcel.class) public void parcelledBundleWithBinder_shouldReturnHasBindersTrue() throws Exception { Bundle bundle = new Bundle(); bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); @@ -470,7 +466,6 @@ public class BundleTest { } @Test - @DisabledOnRavenwood(blockedBy = Parcel.class) public void parcelledBundleWithoutBinder_shouldReturnHasBindersFalse() throws Exception { Bundle bundle = new Bundle(); bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); diff --git a/core/tests/coretests/src/android/os/ParcelNullabilityTest.java b/core/tests/coretests/src/android/os/ParcelNullabilityTest.java index 09395f15a57b..96316c436d61 100644 --- a/core/tests/coretests/src/android/os/ParcelNullabilityTest.java +++ b/core/tests/coretests/src/android/os/ParcelNullabilityTest.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.ravenwood.RavenwoodRule; import android.util.ArrayMap; @@ -67,7 +67,7 @@ public final class ParcelNullabilityTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) + @DisabledOnRavenwood(blockedBy = android.text.Spanned.class) public void nullCharSequence() { Parcel p = Parcel.obtain(); p.writeCharSequence(null); @@ -76,7 +76,6 @@ public final class ParcelNullabilityTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void nullStrongBinder() { Parcel p = Parcel.obtain(); p.writeStrongBinder(null); @@ -85,7 +84,6 @@ public final class ParcelNullabilityTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void nullStringInterface() { Parcel p = Parcel.obtain(); p.writeStrongInterface(null); diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 037323111e9f..da9d687ee2b0 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -23,7 +23,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; import android.util.Log; @@ -48,7 +47,6 @@ public class ParcelTest { private static final String INTERFACE_TOKEN_2 = "Another IBinder interface token"; @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testIsForRpc() { Parcel p = Parcel.obtain(); assertEquals(false, p.isForRpc()); @@ -56,7 +54,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testCallingWorkSourceUidAfterWrite() { Parcel p = Parcel.obtain(); // Method does not throw if replaceCallingWorkSourceUid is called before requests headers @@ -77,7 +74,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testCallingWorkSourceUidAfterEnforce() { Parcel p = Parcel.obtain(); p.writeInterfaceToken(INTERFACE_TOKEN_1); @@ -95,7 +91,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testParcelWithMultipleHeaders() { Parcel p = Parcel.obtain(); Binder.setCallingWorkSourceUid(WORK_SOURCE_1); @@ -153,7 +148,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testCompareDataInRange_whenSameDataWithBinder() { Binder binder = new Binder(); Parcel pA = Parcel.obtain(); @@ -313,7 +307,6 @@ public class ParcelTest { * and 1M length for complex objects are allowed. */ @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testAllocations_whenWithinLimit() { Binder.setIsDirectlyHandlingTransactionOverride(true); Parcel p = Parcel.obtain(); @@ -398,7 +391,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testHasBinders_AfterWritingBinderToParcel() { Binder binder = new Binder(); Parcel pA = Parcel.obtain(); @@ -410,7 +402,6 @@ public class ParcelTest { } @Test - @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testHasBindersInRange_AfterWritingBinderToParcel() { Binder binder = new Binder(); Parcel pA = Parcel.obtain(); diff --git a/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java new file mode 100644 index 000000000000..eeabc2f4e0ed --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 The Android Open 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.widget; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.content.Context; +import android.content.res.Resources; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.test.AndroidTestCase; +import android.view.View; +import android.view.ViewGroup; +import android.widget.flags.Flags; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ViewGroupFader}. + */ +public class ViewGroupFaderTest extends AndroidTestCase { + + private Context mContext; + private ViewGroupFader mViewGroupFader; + private Resources mResources; + + @Mock + private ViewGroup mViewGroup,mViewGroup1; + + @Mock + private ViewGroupFader mockViewGroupFader; + + @Mock + private ViewGroupFader.AnimationCallback mAnimationCallback; + + @Mock + private ViewGroupFader.ChildViewBoundsProvider mChildViewBoundsProvider; + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + final Context mContext = getInstrumentation().getContext(); + mResources = spy(mContext.getResources()); + when(mResources.getBoolean(com.android.internal.R.bool.config_enableViewGroupScalingFading)) + .thenReturn(true); + when(mViewGroup.getResources()).thenReturn(mResources); + + mViewGroupFader = new ViewGroupFader( + mViewGroup, + mAnimationCallback, + mChildViewBoundsProvider); + } + + /** This test checks that for each child of the parent viewgroup, + * updateListElementFades is called for each of its child, when the Flag is set to true + */ + @Test + @EnableFlags(Flags.FLAG_ENABLE_FADING_VIEW_GROUP) + public void testFadingAndScrollingAnimationWorking_FlagOn() { + mViewGroup.addView(mViewGroup1); + mViewGroupFader.updateFade(); + + for (int i = 0; i < mViewGroup.getChildCount(); i++) { + View child = mViewGroup.getChildAt(i); + verify(mockViewGroupFader).updateListElementFades((ViewGroup)child,true); + } + } + + /** This test checks that for each child of the parent viewgroup, + * updateListElementFades is never called for each of its child, when the Flag is set to false + */ + @Test + public void testFadingAndScrollingAnimationNotWorking_FlagOff() { + mViewGroup.addView(mViewGroup1); + mViewGroupFader.updateFade(); + + for (int i = 0; i < mViewGroup.getChildCount(); i++) { + View child = mViewGroup.getChildAt(i); + verify(mockViewGroupFader,never()).updateListElementFades((ViewGroup)child,true); + } + } +}
\ No newline at end of file diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index df95a91d72d7..b866382e4061 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -1805,16 +1805,7 @@ public class Paint { * @return true if elegant metrics are enabled for text drawing. */ public boolean isElegantTextHeight() { - int rawValue = nGetElegantTextHeight(mNativePaint); - switch (rawValue) { - case ELEGANT_TEXT_HEIGHT_DISABLED: - return false; - case ELEGANT_TEXT_HEIGHT_ENABLED: - return true; - case ELEGANT_TEXT_HEIGHT_UNSET: - default: - return com.android.text.flags.Flags.deprecateUiFonts(); - } + return nGetElegantTextHeight(mNativePaint) != ELEGANT_TEXT_HEIGHT_DISABLED; } // Note: the following three values must be equal to the ones in the JNI file: Paint.cpp diff --git a/graphics/java/android/graphics/fonts/FontCustomizationParser.java b/graphics/java/android/graphics/fonts/FontCustomizationParser.java index ba5628cd2bc1..b7bf0553bcc6 100644 --- a/graphics/java/android/graphics/fonts/FontCustomizationParser.java +++ b/graphics/java/android/graphics/fonts/FontCustomizationParser.java @@ -182,10 +182,8 @@ public class FontCustomizationParser { // For ignoring the customization, consume the new-locale-family element but don't // register any customizations. - if (com.android.text.flags.Flags.vendorCustomLocaleFallback()) { - outCustomization.add(new FontConfig.Customization.LocaleFallback( - Locale.forLanguageTag(lang), intOp, family)); - } + outCustomization.add(new FontConfig.Customization.LocaleFallback( + Locale.forLanguageTag(lang), intOp, family)); } else { throw new IllegalArgumentException("Unknown customizationType=" + customizationType); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java index 4ce294213526..bfccb29bc952 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java @@ -16,16 +16,26 @@ package androidx.window.extensions.embedding; +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO; +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO; + import android.os.Build; import android.os.Bundle; +import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; +import android.util.ArrayMap; import android.util.Log; +import android.util.SparseArray; +import android.window.TaskFragmentInfo; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Helper class to back up and restore the TaskFragmentOrganizer state, in order to resume @@ -40,11 +50,21 @@ class BackupHelper { @NonNull private final SplitController mController; @NonNull + private final SplitPresenter mPresenter; + @NonNull private final BackupIdler mBackupIdler = new BackupIdler(); private boolean mBackupIdlerScheduled; - BackupHelper(@NonNull SplitController splitController, @NonNull Bundle savedState) { + private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList = + new ArrayList<>(); + private final ArrayMap<IBinder, TaskFragmentInfo> mTaskFragmentInfos = new ArrayMap<>(); + private final SparseArray<TaskFragmentParentInfo> mTaskFragmentParentInfos = + new SparseArray<>(); + + BackupHelper(@NonNull SplitController splitController, @NonNull SplitPresenter splitPresenter, + @NonNull Bundle savedState) { mController = splitController; + mPresenter = splitPresenter; if (!savedState.isEmpty()) { restoreState(savedState); @@ -67,13 +87,13 @@ class BackupHelper { public boolean queueIdle() { synchronized (mController.mLock) { mBackupIdlerScheduled = false; - startBackup(); + saveState(); } return false; } } - private void startBackup() { + private void saveState() { final List<TaskContainer> taskContainers = mController.getTaskContainers(); if (taskContainers.isEmpty()) { Log.w(TAG, "No task-container to back up"); @@ -97,13 +117,92 @@ class BackupHelper { return; } - final List<ParcelableTaskContainerData> parcelableTaskContainerDataList = - savedState.getParcelableArrayList(KEY_TASK_CONTAINERS, - ParcelableTaskContainerData.class); - for (ParcelableTaskContainerData data : parcelableTaskContainerDataList) { - final TaskContainer taskContainer = new TaskContainer(data, mController); - if (DEBUG) Log.d(TAG, "Restoring task " + taskContainer.getTaskId()); - // TODO(b/289875940): implement the TaskContainer restoration. + if (DEBUG) Log.d(TAG, "Start restoring saved-state"); + mParcelableTaskContainerDataList.addAll(savedState.getParcelableArrayList( + KEY_TASK_CONTAINERS, ParcelableTaskContainerData.class)); + if (DEBUG) Log.d(TAG, "Retrieved tasks : " + mParcelableTaskContainerDataList.size()); + if (mParcelableTaskContainerDataList.isEmpty()) { + return; + } + + final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList( + KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class); + for (TaskFragmentInfo info : infos) { + if (DEBUG) Log.d(TAG, "Retrieved: " + info); + mTaskFragmentInfos.put(info.getFragmentToken(), info); + mPresenter.updateTaskFragmentInfo(info); + } + + final List<TaskFragmentParentInfo> parentInfos = savedState.getParcelableArrayList( + KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO, + TaskFragmentParentInfo.class); + for (TaskFragmentParentInfo info : parentInfos) { + if (DEBUG) Log.d(TAG, "Retrieved: " + info); + mTaskFragmentParentInfos.put(info.getTaskId(), info); + } + } + + boolean hasPendingStateToRestore() { + return !mParcelableTaskContainerDataList.isEmpty(); + } + + /** + * Returns {@code true} if any of the {@link TaskContainer} is restored. + * Otherwise, returns {@code false}. + */ + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, + @NonNull Set<EmbeddingRule> rules) { + if (mParcelableTaskContainerDataList.isEmpty()) { + return false; + } + + if (DEBUG) Log.d(TAG, "Rebuilding TaskContainers."); + final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>(); + for (EmbeddingRule rule : rules) { + embeddingRuleMap.put(rule.getTag(), rule); + } + + boolean restoredAny = false; + for (int i = mParcelableTaskContainerDataList.size() - 1; i >= 0; i--) { + final ParcelableTaskContainerData parcelableTaskContainerData = + mParcelableTaskContainerDataList.get(i); + final List<String> tags = parcelableTaskContainerData.getSplitRuleTags(); + if (!embeddingRuleMap.containsAll(tags)) { + // has unknown tag, unable to restore. + if (DEBUG) { + Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#" + + parcelableTaskContainerData.mTaskId); + } + continue; + } + + mParcelableTaskContainerDataList.remove(parcelableTaskContainerData); + final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData, + mController, mTaskFragmentInfos); + if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer); + mController.addTaskContainer(taskContainer.getTaskId(), taskContainer); + + for (ParcelableSplitContainerData splitData : + parcelableTaskContainerData.getParcelableSplitContainerDataList()) { + final SplitRule rule = (SplitRule) embeddingRuleMap.get(splitData.mSplitRuleTag); + assert rule != null; + if (mController.getContainer(splitData.getPrimaryContainerToken()) != null + && mController.getContainer(splitData.getSecondaryContainerToken()) + != null) { + taskContainer.addSplitContainer( + new SplitContainer(splitData, mController, rule)); + } + } + + mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(), + mTaskFragmentParentInfos.get(taskContainer.getTaskId())); + restoredAny = true; + } + + if (mParcelableTaskContainerDataList.isEmpty()) { + mTaskFragmentParentInfos.clear(); + mTaskFragmentInfos.clear(); } + return restoredAny; } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java index 817cfce69b2e..cb280c530c1b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java @@ -89,13 +89,13 @@ class ParcelableSplitContainerData implements Parcelable { }; @NonNull - private IBinder getPrimaryContainerToken() { + IBinder getPrimaryContainerToken() { return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken() : mPrimaryContainerToken; } @NonNull - private IBinder getSecondaryContainerToken() { + IBinder getSecondaryContainerToken() { return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken() : mSecondaryContainerToken; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java index 7377d005cda4..97aa69985907 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java @@ -108,6 +108,15 @@ class ParcelableTaskContainerData implements Parcelable { : mParcelableSplitContainerDataList; } + @NonNull + List<String> getSplitRuleTags() { + final List<String> tags = new ArrayList<>(); + for (ParcelableSplitContainerData data : getParcelableSplitContainerDataList()) { + tags.add(data.mSplitRuleTag); + } + return tags; + } + @Override public int describeContents() { return 0; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 6d436ec01d98..faf73c24073f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -86,6 +86,25 @@ class SplitContainer { } } + /** This is only used when restoring it from a {@link ParcelableSplitContainerData}. */ + SplitContainer(@NonNull ParcelableSplitContainerData parcelableData, + @NonNull SplitController splitController, @NonNull SplitRule splitRule) { + mParcelableData = parcelableData; + mPrimaryContainer = splitController.getContainer(parcelableData.getPrimaryContainerToken()); + mSecondaryContainer = splitController.getContainer( + parcelableData.getSecondaryContainerToken()); + mSplitRule = splitRule; + mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); + mCurrentSplitAttributes = mDefaultSplitAttributes; + + if (shouldFinishPrimaryWithSecondary(splitRule)) { + mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer); + } + if (shouldFinishSecondaryWithPrimary(splitRule)) { + mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer); + } + } + void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) { if (!mParcelableData.mIsPrimaryContainerMutable) { throw new IllegalStateException("Cannot update primary TaskFragmentContainer"); 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 f2f2b7ea7174..db4bb0e5e75e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -279,6 +279,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen Log.i(TAG, "Setting embedding rules. Size: " + rules.size()); mSplitRules.clear(); mSplitRules.addAll(rules); + + if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) { + return; + } + + try { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + if (mPresenter.rebuildTaskContainers(wct, rules)) { + transactionRecord.apply(false /* shouldApplyIndependently */); + updateCallbackIfNecessary(); + } else { + transactionRecord.abort(); + } + } catch (IllegalStateException ex) { + Log.e(TAG, "Having an existing transaction while running restoration with" + + "new rules!! It is likely too late to perform the restoration " + + "already!?", ex); + } } } @@ -903,6 +923,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @GuardedBy("mLock") + void onTaskFragmentParentRestored(@NonNull WindowContainerTransaction wct, int taskId, + @NonNull TaskFragmentParentInfo parentInfo) { + onTaskFragmentParentInfoChanged(wct, taskId, parentInfo); + } + + @GuardedBy("mLock") void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) { final TaskContainer taskContainer = getTaskContainer(taskId); if (taskContainer == null) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index abc7b291fc32..0c0ded9bad74 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -24,6 +24,7 @@ import static androidx.window.extensions.embedding.SplitController.TAG; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import android.annotation.AnimRes; +import android.annotation.NonNull; import android.app.Activity; import android.app.ActivityThread; import android.app.WindowConfiguration; @@ -47,7 +48,6 @@ import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.window.extensions.core.util.function.Function; import androidx.window.extensions.embedding.SplitAttributes.SplitType; @@ -67,6 +67,7 @@ import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; /** @@ -174,7 +175,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } else { registerOrganizer(); } - mBackupHelper = new BackupHelper(controller, outSavedState); + mBackupHelper = new BackupHelper(controller, this, outSavedState); if (!SplitController.ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition // Animation will be handled by WM Shell when Shell transition is enabled. @@ -186,6 +187,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mBackupHelper.scheduleBackup(); } + boolean isRebuildTaskContainersNeeded() { + return mBackupHelper.hasPendingStateToRestore(); + } + + boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct, + @NonNull Set<EmbeddingRule> rules) { + return mBackupHelper.rebuildTaskContainers(wct, rules); + } + /** * Deletes the specified container and all other associated and dependent containers in the same * transaction. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 608a3bee7509..74cce68f270b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -31,6 +31,7 @@ import android.app.WindowConfiguration.WindowingMode; import android.content.res.Configuration; import android.graphics.Rect; import android.os.IBinder; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.Log; @@ -147,14 +148,23 @@ class TaskContainer { /** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */ TaskContainer(@NonNull ParcelableTaskContainerData data, - @NonNull SplitController splitController) { + @NonNull SplitController splitController, + @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) { mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this); + mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */, + false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); mSplitController = splitController; for (ParcelableTaskFragmentContainerData tfData : data.getParcelableTaskFragmentContainerDataList()) { - final TaskFragmentContainer container = - new TaskFragmentContainer(tfData, splitController, this); - mContainers.add(container); + final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken); + if (info != null && !info.isEmpty()) { + final TaskFragmentContainer container = + new TaskFragmentContainer(tfData, splitController, this); + container.setInfo(new WindowContainerTransaction(), info); + mContainers.add(container); + } else { + Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId); + } } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt new file mode 100644 index 000000000000..35d459f27534 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2024 The Android Open 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.bubbles.bar + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Insets +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubble +import com.android.wm.shell.bubbles.BubbleData +import com.android.wm.shell.bubbles.BubbleExpandedViewManager +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleTaskView +import com.android.wm.shell.bubbles.BubbleTaskViewFactory +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.RegionSamplingProvider +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.handles.RegionSamplingHelper +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Collections +import java.util.concurrent.Executor + +/** Tests for [BubbleBarExpandedViewTest] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarExpandedViewTest { + companion object { + const val SCREEN_WIDTH = 2000 + const val SCREEN_HEIGHT = 1000 + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private val windowManager = context.getSystemService(WindowManager::class.java) + + private lateinit var mainExecutor: TestExecutor + private lateinit var bgExecutor: TestExecutor + + private lateinit var expandedViewManager: BubbleExpandedViewManager + private lateinit var positioner: BubblePositioner + private lateinit var bubbleTaskView: BubbleTaskView + + private lateinit var bubbleExpandedView: BubbleBarExpandedView + private var testableRegionSamplingHelper: TestableRegionSamplingHelper? = null + private var regionSamplingProvider: TestRegionSamplingProvider? = null + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + mainExecutor = TestExecutor() + bgExecutor = TestExecutor() + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + + expandedViewManager = createExpandedViewManager() + bubbleTaskView = FakeBubbleTaskViewFactory().create() + + val inflater = LayoutInflater.from(context) + + regionSamplingProvider = TestRegionSamplingProvider() + + bubbleExpandedView = (inflater.inflate( + R.layout.bubble_bar_expanded_view, null, false /* attachToRoot */ + ) as BubbleBarExpandedView) + bubbleExpandedView.initialize( + expandedViewManager, + positioner, + false /* isOverflow */, + bubbleTaskView, + mainExecutor, + bgExecutor, + regionSamplingProvider + ) + + getInstrumentation().runOnMainSync(Runnable { + bubbleExpandedView.onAttachedToWindow() + // Helper should be created once attached to window + testableRegionSamplingHelper = regionSamplingProvider!!.helper + }) + } + + @After + fun tearDown() { + testableRegionSamplingHelper?.stopAndDestroy() + } + + @Test + fun testCreateSamplingHelper_onAttach() { + assertThat(testableRegionSamplingHelper).isNotNull() + } + + @Test + fun testDestroySamplingHelper_onDetach() { + bubbleExpandedView.onDetachedFromWindow() + assertThat(testableRegionSamplingHelper!!.isDestroyed).isTrue() + } + + @Test + fun testStopSampling_onDragStart() { + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + + bubbleExpandedView.setDragging(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + @Test + fun testStartSampling_onDragEnd() { + bubbleExpandedView.setDragging(true) + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + + bubbleExpandedView.setDragging(false) + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testStartSampling_onContentVisible() { + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.setWindowVisible).isTrue() + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testStopSampling_onContentInvisible() { + bubbleExpandedView.setContentVisibility(false) + + assertThat(testableRegionSamplingHelper!!.setWindowInvisible).isTrue() + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + @Test + fun testSampling_startStopAnimating_visible() { + bubbleExpandedView.isAnimating = true + bubbleExpandedView.setContentVisibility(true) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + + bubbleExpandedView.isAnimating = false + assertThat(testableRegionSamplingHelper!!.isStarted).isTrue() + } + + @Test + fun testSampling_startStopAnimating_invisible() { + bubbleExpandedView.isAnimating = true + bubbleExpandedView.setContentVisibility(false) + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + testableRegionSamplingHelper!!.reset() + + bubbleExpandedView.isAnimating = false + assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() + } + + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock<TaskViewTaskController>() + val taskView = TaskView(context, taskViewTaskController) + val taskInfo = mock<ActivityManager.RunningTaskInfo>() + whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) + return BubbleTaskView(taskView, mainExecutor) + } + } + + private inner class TestRegionSamplingProvider : RegionSamplingProvider { + + lateinit var helper: TestableRegionSamplingHelper + + override fun createHelper( + sampledView: View?, + callback: RegionSamplingHelper.SamplingCallback?, + backgroundExecutor: Executor?, + mainExecutor: Executor? + ): RegionSamplingHelper { + helper = TestableRegionSamplingHelper(sampledView, callback, backgroundExecutor, + mainExecutor) + return helper + } + } + + private inner class TestableRegionSamplingHelper( + sampledView: View?, + samplingCallback: SamplingCallback?, + backgroundExecutor: Executor?, + mainExecutor: Executor? + ) : RegionSamplingHelper(sampledView, samplingCallback, backgroundExecutor, mainExecutor) { + + var isStarted = false + var isStopped = false + var isDestroyed = false + var setWindowVisible = false + var setWindowInvisible = false + + override fun start(initialSamplingBounds: Rect) { + super.start(initialSamplingBounds) + isStarted = true + } + + override fun stop() { + super.stop() + isStopped = true + } + + override fun stopAndDestroy() { + super.stopAndDestroy() + isDestroyed = true + } + + override fun setWindowVisible(visible: Boolean) { + super.setWindowVisible(visible) + if (visible) { + setWindowVisible = true + } else { + setWindowInvisible = true + } + } + + fun reset() { + isStarted = false + isStopped = false + isDestroyed = false + setWindowVisible = false + setWindowInvisible = false + } + } + + private fun createExpandedViewManager(): BubbleExpandedViewManager { + return object : BubbleExpandedViewManager { + override val overflowBubbles: List<Bubble> + get() = Collections.emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) { + } + + override fun collapseStack() { + } + + override fun updateWindowFlagsForBackpress(intercept: Boolean) { + } + + override fun promoteBubbleFromOverflow(bubble: Bubble) { + } + + override fun removeBubble(key: String, reason: Int) { + } + + override fun dismissBubble(bubble: Bubble, reason: Int) { + } + + override fun setAppBubbleTaskId(key: String, taskId: Int) { + } + + override fun isStackExpanded(): Boolean { + return true + } + + override fun isShowingAsBubbleBar(): Boolean { + return true + } + + override fun hideCurrentInputMethod() { + } + + override fun updateBubbleBarLocation(location: BubbleBarLocation) { + } + } + } + + private class TestExecutor : ShellExecutor { + + private val runnables: MutableList<Runnable> = mutableListOf() + + override fun execute(runnable: Runnable) { + runnables.add(runnable) + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + execute(runnable) + } + + override fun removeCallbacks(runnable: Runnable?) {} + + override fun hasCallback(runnable: Runnable?): Boolean = false + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 2d98a2b675a3..755e0d5f742d 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -569,7 +569,7 @@ <!-- The thickness in dp for all desktop drag transition regions. --> <dimen name="desktop_mode_transition_region_thickness">44dp</dimen> - <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.4</item> + <item type="dimen" format="float" name="desktop_mode_fullscreen_region_scale">0.2</item> <!-- The height on the screen where drag to the left or right edge will result in a desktop task snapping to split size. The empty space between this and the top is to allow diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java index b92b8ef657a3..a06cf78d0898 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/handles/RegionSamplingHelper.java @@ -329,7 +329,7 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, /** * Get the sampled region of interest from the sampled view * @param sampledView The view that this helper is attached to for convenience - * @return the region to be sampled in sceen coordinates. Return {@code null} to avoid + * @return the region to be sampled in screen coordinates. Return {@code null} to avoid * sampling in this frame */ Rect getSampledRegion(View sampledView); diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index cf39415b3fe6..6c83d88032df 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -29,7 +29,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.TaskSnapshot; /** @@ -75,7 +74,7 @@ public abstract class PipContentOverlay { public PipColorOverlay(Context context) { mContext = context; - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .setColorLayer() @@ -123,7 +122,7 @@ public abstract class PipContentOverlay { public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { mSnapshot = snapshot; mSourceRectHint = new Rect(sourceRectHint); - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .build(); @@ -183,7 +182,7 @@ public abstract class PipContentOverlay { mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); - mLeash = new SurfaceControl.Builder(new SurfaceSession()) + mLeash = new SurfaceControl.Builder() .setCallsite(TAG) .setName(LAYER_NAME) .build(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt new file mode 100644 index 000000000000..249185eca323 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AssistContentRequester.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open 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.apptoweb + +import android.app.ActivityTaskManager +import android.app.IActivityTaskManager +import android.app.IAssistDataReceiver +import android.app.assist.AssistContent +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.os.RemoteException +import android.util.Slog +import java.lang.ref.WeakReference +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.Executor + +/** + * Can be used to request the AssistContent from a provided task id, useful for getting the web uri + * if provided from the task. + */ +class AssistContentRequester( + context: Context, + private val callBackExecutor: Executor, + private val systemInteractionExecutor: Executor +) { + interface Callback { + // Called when the [AssistContent] of the requested task is available. + fun onAssistContentAvailable(assistContent: AssistContent?) + } + + private val activityTaskManager: IActivityTaskManager = ActivityTaskManager.getService() + private val attributionTag: String? = context.attributionTag + private val packageName: String = context.applicationContext.packageName + + // If system loses the callback, our internal cache of original callback will also get cleared. + private val pendingCallbacks = Collections.synchronizedMap(WeakHashMap<Any, Callback>()) + + /** + * Request the [AssistContent] from the task with the provided id. + * + * @param taskId to query for the content. + * @param callback to call when the content is available, called on the main thread. + */ + fun requestAssistContent(taskId: Int, callback: Callback) { + // ActivityTaskManager interaction here is synchronous, so call off the main thread. + systemInteractionExecutor.execute { + try { + val success = activityTaskManager.requestAssistDataForTask( + AssistDataReceiver(callback, this), + taskId, + packageName, + attributionTag, + false /* fetchStructure */ + ) + if (!success) { + executeOnMainExecutor { callback.onAssistContentAvailable(null) } + } + } catch (e: RemoteException) { + Slog.e(TAG, "Requesting assist content failed for task: $taskId", e) + } + } + } + + private fun executeOnMainExecutor(callback: Runnable) { + callBackExecutor.execute(callback) + } + + private class AssistDataReceiver( + callback: Callback, + parent: AssistContentRequester + ) : IAssistDataReceiver.Stub() { + // The AssistDataReceiver binder callback object is passed to a system server, that may + // keep hold of it for longer than the lifetime of the AssistContentRequester object, + // potentially causing a memory leak. In the callback passed to the system server, only + // keep a weak reference to the parent object and lookup its callback if it still exists. + private val parentRef: WeakReference<AssistContentRequester> + private val callbackKey = Any() + + init { + parent.pendingCallbacks[callbackKey] = callback + parentRef = WeakReference(parent) + } + + override fun onHandleAssistData(data: Bundle?) { + val content = data?.getParcelable(ASSIST_KEY_CONTENT, AssistContent::class.java) + if (content == null) { + Slog.d(TAG, "Received AssistData, but no AssistContent found") + return + } + val requester = parentRef.get() + if (requester != null) { + val callback = requester.pendingCallbacks[callbackKey] + if (callback != null) { + requester.executeOnMainExecutor { callback.onAssistContentAvailable(content) } + } else { + Slog.d(TAG, "Callback received after calling UI was disposed of") + } + } else { + Slog.d(TAG, "Callback received after Requester was collected") + } + } + + override fun onHandleAssistScreenshot(screenshot: Bitmap) {} + } + + companion object { + private const val TAG = "AssistContentRequester" + private const val ASSIST_KEY_CONTENT = "content" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 7b3b2071ef02..156399499c5b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -982,7 +982,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); mReceivedNullNavigationInfo = false; - mBackTransitionHandler.mLastTrigger = triggerBack; if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); @@ -1103,7 +1102,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont endLatencyTracking(); if (!validateAnimationTargets(apps)) { Log.e(TAG, "Invalid animation targets!"); - mBackTransitionHandler.consumeQueuedTransitionIfNeeded(); return; } mBackAnimationFinishedCallback = finishedCallback; @@ -1113,7 +1111,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } kickStartAnimation(); - mBackTransitionHandler.consumeQueuedTransitionIfNeeded(); }); } @@ -1121,7 +1118,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public void onAnimationCancelled() { mShellExecutor.execute( () -> { - mBackTransitionHandler.consumeQueuedTransitionIfNeeded(); if (!mShellBackAnimationRegistry.cancel( mBackNavigationInfo != null ? mBackNavigationInfo.getType() @@ -1160,8 +1156,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean mCloseTransitionRequested; SurfaceControl.Transaction mFinishOpenTransaction; Transitions.TransitionFinishCallback mFinishOpenTransitionCallback; - QueuedTransition mQueuedTransition = null; - boolean mLastTrigger; // The Transition to make behindActivity become visible IBinder mPrepareOpenTransition; // The Transition to make behindActivity become invisible, if prepare open exist and @@ -1178,13 +1172,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } - void consumeQueuedTransitionIfNeeded() { - if (mQueuedTransition != null) { - mQueuedTransition.consume(); - mQueuedTransition = null; - } - } - private void applyFinishOpenTransition() { mOpenTransitionInfo = null; mPrepareOpenTransition = null; @@ -1215,7 +1202,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { + final boolean isPrepareTransition = + info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; + if (isPrepareTransition) { kickStartAnimation(); } // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't @@ -1240,21 +1229,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } if (mApps == null || mApps.length == 0) { - if (mBackNavigationInfo != null && mShellBackAnimationRegistry - .isWaitingAnimation(mBackNavigationInfo.getType())) { - // Waiting for animation? Queue update to wait for animation start. - consumeQueuedTransitionIfNeeded(); - mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback); - return true; - } else if (mLastTrigger) { - // animation was done, consume directly + if (mCloseTransitionRequested) { + // animation never start, consume directly applyAndFinish(st, ft, finishCallback); return true; - } else { - // animation was cancelled but transition haven't happen, we must handle it - if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) { - createClosePrepareTransition(); - } + } else if (mClosePrepareTransition == null && isPrepareTransition) { + // Gesture animation was cancelled before prepare transition ready, create the + // the close prepare transition + createClosePrepareTransition(); } } @@ -1413,9 +1395,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); } - if (mQueuedTransition != null) { - consumeQueuedTransitionIfNeeded(); - } return; } // Handle the commit transition if this handler is running the open transition. @@ -1423,11 +1402,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont t.apply(); if (mCloseTransitionRequested) { if (mApps == null || mApps.length == 0) { - if (mQueuedTransition == null) { - // animation was done - applyFinishOpenTransition(); - mCloseTransitionRequested = false; - } // let queued transition finish. + // animation was done + applyFinishOpenTransition(); + mCloseTransitionRequested = false; } else { // we are animating, wait until animation finish mOnAnimationFinishCallback = () -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 0c95934abf93..169361ad5f6b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -609,7 +609,8 @@ public class Bubble implements BubbleViewProvider { callback.onBubbleViewsReady(bubble); } }, - mMainExecutor); + mMainExecutor, + mBgExecutor); if (mInflateSynchronously) { mInflationTaskLegacy.onPostExecute(mInflationTaskLegacy.doInBackground()); } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index f32974e1765d..68c4657f2b68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -80,7 +80,10 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl expandedViewManager, positioner, /* isOverflow= */ true, - /* bubbleTaskView= */ null + /* bubbleTaskView= */ null, + /* mainExecutor= */ null, + /* backgroundExecutor= */ null, + /* regionSamplingProvider= */ null ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 5f8f0fd0c54c..0c0fd7b10f6e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -60,6 +60,9 @@ public class BubbleTaskViewHelper { /** Called when back is pressed on the task root. */ void onBackPressed(); + + /** Called when task removal has started. */ + void onTaskRemovalStarted(); } private final Context mContext; @@ -190,6 +193,7 @@ public class BubbleTaskViewHelper { ((ViewGroup) mParentView).removeView(mTaskView); mTaskView = null; } + mListener.onTaskRemovalStarted(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index 13855f73fb4a..3982a237dd3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -38,6 +38,7 @@ import android.graphics.drawable.Icon; import android.util.Log; import android.util.PathParser; import android.view.LayoutInflater; +import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; @@ -47,6 +48,7 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import java.lang.ref.WeakReference; import java.util.Objects; @@ -222,7 +224,16 @@ public class BubbleViewInfoTask { ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing bubble bar expanded view key=%s", mBubble.getKey()); viewInfo.bubbleBarExpandedView.initialize(mExpandedViewManager.get(), - mPositioner.get(), false /* isOverflow */, viewInfo.taskView); + mPositioner.get(), false /* isOverflow */, viewInfo.taskView, + mMainExecutor, mBgExecutor, new RegionSamplingProvider() { + @Override + public RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, Executor mainExecutor) { + return RegionSamplingProvider.super.createHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java index 5cfebf8f1647..1b7bb0db6516 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java @@ -38,6 +38,7 @@ import android.os.AsyncTask; import android.util.Log; import android.util.PathParser; import android.view.LayoutInflater; +import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; @@ -46,6 +47,7 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import java.lang.ref.WeakReference; import java.util.Objects; @@ -85,6 +87,7 @@ public class BubbleViewInfoTaskLegacy extends private boolean mSkipInflation; private Callback mCallback; private Executor mMainExecutor; + private Executor mBackgroundExecutor; /** * Creates a task to load information for the provided {@link Bubble}. Once all info @@ -100,7 +103,8 @@ public class BubbleViewInfoTaskLegacy extends BubbleIconFactory factory, boolean skipInflation, Callback c, - Executor mainExecutor) { + Executor mainExecutor, + Executor backgroundExecutor) { mBubble = b; mContext = new WeakReference<>(context); mExpandedViewManager = new WeakReference<>(expandedViewManager); @@ -112,6 +116,7 @@ public class BubbleViewInfoTaskLegacy extends mSkipInflation = skipInflation; mCallback = c; mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; } @Override @@ -123,7 +128,7 @@ public class BubbleViewInfoTaskLegacy extends if (mLayerView.get() != null) { return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(), mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory, - mBubble, mSkipInflation); + mBubble, mSkipInflation, mMainExecutor, mBackgroundExecutor); } else { return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(), mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory, @@ -188,7 +193,9 @@ public class BubbleViewInfoTaskLegacy extends BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, - boolean skipInflation) { + boolean skipInflation, + Executor mainExecutor, + Executor backgroundExecutor) { BubbleViewInfo info = new BubbleViewInfo(); if (!skipInflation && !b.isInflated()) { @@ -197,7 +204,16 @@ public class BubbleViewInfoTaskLegacy extends info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); info.bubbleBarExpandedView.initialize( - expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView); + expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView, + mainExecutor, backgroundExecutor, new RegionSamplingProvider() { + @Override + public RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, Executor mainExecutor) { + return RegionSamplingProvider.super.createHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + }); } if (!populateCommonInfo(info, c, b, iconFactory)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java new file mode 100644 index 000000000000..30f5c8fd56c3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/RegionSamplingProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open 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.bubbles; + +import android.view.View; + +import com.android.wm.shell.shared.handles.RegionSamplingHelper; + +import java.util.concurrent.Executor; + +/** + * Wrapper to provide a {@link com.android.wm.shell.shared.handles.RegionSamplingHelper} to allow + * testing it. + */ +public interface RegionSamplingProvider { + + /** Creates and returns the region sampling helper */ + default RegionSamplingHelper createHelper(View sampledView, + RegionSamplingHelper.SamplingCallback callback, + Executor backgroundExecutor, + Executor mainExecutor) { + return new RegionSamplingHelper(sampledView, + callback, backgroundExecutor, mainExecutor); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 565fde0a853c..74c3748dccaf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -253,6 +253,7 @@ public class BubbleBarAnimationHelper { return; } setDragPivot(bbev); + bbev.setDragging(true); // Corner radius gets scaled, apply the reverse scale to ensure we have the desired radius final float cornerRadius = bbev.getDraggedCornerRadius() / EXPANDED_VIEW_DRAG_SCALE; @@ -329,6 +330,7 @@ public class BubbleBarAnimationHelper { public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); bbev.resetPivot(); + bbev.setDragging(false); } }); startNewDragAnimation(animatorSet); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index f90b2aa95555..ec235a5d84ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -19,10 +19,7 @@ package com.android.wm.shell.bubbles.bar; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.annotation.Nullable; -import android.app.ActivityManager; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Insets; import android.graphics.Outline; import android.graphics.Rect; @@ -37,6 +34,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; @@ -46,9 +44,12 @@ import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.bubbles.RegionSamplingProvider; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.handles.RegionSamplingHelper; import com.android.wm.shell.taskview.TaskView; +import java.util.concurrent.Executor; import java.util.function.Supplier; /** Expanded view of a bubble when it's part of the bubble bar. */ @@ -92,16 +93,35 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private boolean mIsOverflow; private BubbleTaskViewHelper mBubbleTaskViewHelper; private BubbleBarMenuViewController mMenuViewController; - private @Nullable Supplier<Rect> mLayerBoundsSupplier; - private @Nullable Listener mListener; + @Nullable + private Supplier<Rect> mLayerBoundsSupplier; + @Nullable + private Listener mListener; private BubbleBarHandleView mHandleView; - private @Nullable TaskView mTaskView; - private @Nullable BubbleOverflowContainerView mOverflowView; + @Nullable + private TaskView mTaskView; + @Nullable + private BubbleOverflowContainerView mOverflowView; + /** + * The handle shown in the caption area is tinted based on the background color of the area. + * This can vary so we sample the caption region and update the handle color based on that. + * If we're showing the overflow, the helper and executors will be null. + */ + @Nullable + private RegionSamplingHelper mRegionSamplingHelper; + @Nullable + private RegionSamplingProvider mRegionSamplingProvider; + @Nullable + private Executor mMainExecutor; + @Nullable + private Executor mBackgroundExecutor; + private final Rect mSampleRect = new Rect(); + private final int[] mLoc = new int[2]; + + /** Height of the caption inset at the top of the TaskView */ private int mCaptionHeight; - - private int mBackgroundColor; /** Corner radius used when view is resting */ private float mRestingCornerRadius = 0f; /** Corner radius applied while dragging */ @@ -116,6 +136,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView */ private boolean mIsContentVisible = false; private boolean mIsAnimating; + private boolean mIsDragging; public BubbleBarExpandedView(Context context) { this(context, null); @@ -154,21 +175,20 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView setOnTouchListener((v, event) -> true); } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - // Hide manage menu when view disappears - mMenuViewController.hideMenu(false /* animated */); - } - /** Initializes the view, must be called before doing anything else. */ public void initialize(BubbleExpandedViewManager expandedViewManager, BubblePositioner positioner, boolean isOverflow, - @Nullable BubbleTaskView bubbleTaskView) { + @Nullable BubbleTaskView bubbleTaskView, + @Nullable Executor mainExecutor, + @Nullable Executor backgroundExecutor, + @Nullable RegionSamplingProvider regionSamplingProvider) { mManager = expandedViewManager; mPositioner = positioner; mIsOverflow = isOverflow; + mMainExecutor = mainExecutor; + mBackgroundExecutor = backgroundExecutor; + mRegionSamplingProvider = regionSamplingProvider; if (mIsOverflow) { mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( @@ -191,6 +211,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mTaskView.setEnableSurfaceClipping(true); mTaskView.setCornerRadius(mCurrentCornerRadius); mTaskView.setVisibility(VISIBLE); + mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); @@ -245,32 +266,40 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView return mHandleView; } - // TODO (b/275087636): call this when theme/config changes /** Updates the view based on the current theme. */ public void applyThemeAttrs() { + mCaptionHeight = getResources().getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_caption_height); mRestingCornerRadius = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_corner_radius - ); + R.dimen.bubble_bar_expanded_view_corner_radius); mDraggedCornerRadius = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_corner_radius_dragged - ); + R.dimen.bubble_bar_expanded_view_corner_radius_dragged); mCurrentCornerRadius = mRestingCornerRadius; - final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ - android.R.attr.colorBackgroundFloating}); - mBackgroundColor = ta.getColor(0, Color.WHITE); - ta.recycle(); - mCaptionHeight = getResources().getDimensionPixelSize( - R.dimen.bubble_bar_expanded_view_caption_height); - if (mTaskView != null) { mTaskView.setCornerRadius(mCurrentCornerRadius); - updateHandleColor(true /* animated */); + mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); } } @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + // Hide manage menu when view disappears + mMenuViewController.hideMenu(false /* animated */); + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + recreateRegionSamplingHelper(); + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mTaskView != null) { @@ -284,16 +313,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mTaskView != null) { - mTaskView.layout(l, t, r, - t + mTaskView.getMeasuredHeight()); - mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); + mTaskView.layout(l, t, r, t + mTaskView.getMeasuredHeight()); } } @Override public void onTaskCreated() { setContentVisibility(true); - updateHandleColor(false /* animated */); if (mListener != null) { mListener.onTaskCreated(); } @@ -305,11 +331,70 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } @Override + public void onTaskRemovalStarted() { + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } + } + + @Override public void onBackPressed() { if (mListener == null) return; mListener.onBackPressed(); } + /** + * Set whether this view is currently being dragged. + * + * When dragging, the handle is hidden and content shouldn't be sampled. When dragging has + * ended we should start again. + */ + public void setDragging(boolean isDragging) { + if (isDragging != mIsDragging) { + mIsDragging = isDragging; + updateSamplingState(); + } + } + + /** Returns whether region sampling should be enabled, i.e. if task view content is visible. */ + private boolean shouldSampleRegion() { + return mTaskView != null + && mTaskView.getTaskInfo() != null + && !mIsDragging + && !mIsAnimating + && mIsContentVisible; + } + + /** + * Handles starting or stopping the region sampling helper based on + * {@link #shouldSampleRegion()}. + */ + private void updateSamplingState() { + if (mRegionSamplingHelper == null) return; + boolean shouldSample = shouldSampleRegion(); + if (shouldSample) { + mRegionSamplingHelper.start(getCaptionSampleRect()); + } else { + mRegionSamplingHelper.stop(); + } + } + + /** Returns the current area of the caption bar, in screen coordinates. */ + Rect getCaptionSampleRect() { + if (mTaskView == null) return null; + mTaskView.getLocationOnScreen(mLoc); + mSampleRect.set(mLoc[0], mLoc[1], + mLoc[0] + mTaskView.getWidth(), + mLoc[1] + mCaptionHeight); + return mSampleRect; + } + + @VisibleForTesting + @Nullable + public RegionSamplingHelper getRegionSamplingHelper() { + return mRegionSamplingHelper; + } + /** Cleans up the expanded view, should be called when the bubble is no longer active. */ public void cleanUpExpandedState() { mMenuViewController.hideMenu(false /* animated */); @@ -394,27 +479,14 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView if (!mIsAnimating) { mTaskView.setAlpha(visible ? 1f : 0f); + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.setWindowVisible(visible); + } + updateSamplingState(); } } /** - * Updates the handle color based on the task view status bar or background color; if those - * are transparent it defaults to the background color pulled from system theme attributes. - */ - private void updateHandleColor(boolean animated) { - if (mTaskView == null || mTaskView.getTaskInfo() == null) return; - int color = mBackgroundColor; - ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription; - if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) { - color = taskDescription.getStatusBarColor(); - } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) { - color = taskDescription.getBackgroundColor(); - } - final boolean isRegionDark = Color.luminance(color) <= 0.5; - mHandleView.updateHandleColor(isRegionDark, animated); - } - - /** * Sets the alpha of both this view and the task view. */ public void setTaskViewAlpha(float alpha) { @@ -442,6 +514,11 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView */ public void setAnimating(boolean animating) { mIsAnimating = animating; + if (mIsAnimating) { + // Stop sampling while animating -- when animating is done setContentVisibility will + // re-trigger sampling if we're visible. + updateSamplingState(); + } // If we're done animating, apply the correct visibility. if (!animating) { setContentVisibility(mIsContentVisible); @@ -481,6 +558,37 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } } + private void recreateRegionSamplingHelper() { + if (mRegionSamplingHelper != null) { + mRegionSamplingHelper.stopAndDestroy(); + } + if (mMainExecutor == null || mBackgroundExecutor == null + || mRegionSamplingProvider == null) { + // Null when it's the overflow / don't need sampling then. + return; + } + mRegionSamplingHelper = mRegionSamplingProvider.createHelper(this, + new RegionSamplingHelper.SamplingCallback() { + @Override + public void onRegionDarknessChanged(boolean isRegionDark) { + if (mHandleView != null) { + mHandleView.updateHandleColor(isRegionDark, + true /* animated */); + } + } + + @Override + public Rect getSampledRegion(View sampledView) { + return getCaptionSampleRect(); + } + + @Override + public boolean isSamplingEnabled() { + return shouldSampleRegion(); + } + }, mMainExecutor, mBackgroundExecutor); + } + private class HandleViewAccessibilityDelegate extends AccessibilityDelegate { @Override public void onInitializeAccessibilityNodeInfo(@NonNull View host, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java index c91567d7d8be..e781c07f01a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java @@ -42,7 +42,9 @@ public class BubbleBarHandleView extends View { private final @ColorInt int mHandleLightColor; private final @ColorInt int mHandleDarkColor; - private @Nullable ObjectAnimator mColorChangeAnim; + private @ColorInt int mCurrentColor; + @Nullable + private ObjectAnimator mColorChangeAnim; public BubbleBarHandleView(Context context) { this(context, null /* attrs */); @@ -88,13 +90,17 @@ public class BubbleBarHandleView extends View { * * @param isRegionDark Whether the background behind the handle is dark, and thus the handle * should be light (and vice versa). - * @param animated Whether to animate the change, or apply it immediately. + * @param animated Whether to animate the change, or apply it immediately. */ public void updateHandleColor(boolean isRegionDark, boolean animated) { int newColor = isRegionDark ? mHandleLightColor : mHandleDarkColor; + if (newColor == mCurrentColor) { + return; + } if (mColorChangeAnim != null) { mColorChangeAnim.cancel(); } + mCurrentColor = newColor; if (animated) { mColorChangeAnim = ObjectAnimator.ofArgb(this, "backgroundColor", newColor); mColorChangeAnim.addListener(new AnimatorListenerAdapter() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java index 4b138e43bc3f..dd17e2980e58 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java @@ -17,7 +17,6 @@ package com.android.wm.shell.common; import android.view.SurfaceControl; -import android.view.SurfaceSession; /** * Helpers for handling surface. @@ -25,16 +24,15 @@ import android.view.SurfaceSession; public class SurfaceUtils { /** Creates a dim layer above host surface. */ public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host, - String name, SurfaceSession surfaceSession) { - final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession); + String name) { + final SurfaceControl dimLayer = makeColorLayer(host, name); t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f}); return dimLayer; } /** Creates a color layer for host surface. */ - public static SurfaceControl makeColorLayer(SurfaceControl host, String name, - SurfaceSession surfaceSession) { - return new SurfaceControl.Builder(surfaceSession) + public static SurfaceControl makeColorLayer(SurfaceControl host, String name) { + return new SurfaceControl.Builder() .setParent(host) .setColorLayer() .setName(name) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index ef33b3830e45..3dc86decdb2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -42,7 +42,6 @@ import android.view.InsetsState; import android.view.ScrollCaptureResponse; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; @@ -311,7 +310,7 @@ public class SystemWindows { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession()) + SurfaceControl leash = new SurfaceControl.Builder() .setContainerLayer() .setName("SystemWindowLeash") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 7175e361f91a..de3152ad7687 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -43,7 +43,6 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -74,7 +73,6 @@ public class SplitDecorManager extends WindowlessWindowManager { private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground"; private final IconProvider mIconProvider; - private final SurfaceSession mSurfaceSession; private Drawable mIcon; private ImageView mVeilIconView; @@ -103,17 +101,15 @@ public class SplitDecorManager extends WindowlessWindowManager { private int mOffsetY; private int mRunningAnimationCount = 0; - public SplitDecorManager(Configuration configuration, IconProvider iconProvider, - SurfaceSession surfaceSession) { + public SplitDecorManager(Configuration configuration, IconProvider iconProvider) { super(configuration, null /* rootSurface */, null /* hostInputToken */); mIconProvider = iconProvider; - mSurfaceSession = surfaceSession; } @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG) .setHidden(true) @@ -238,7 +234,7 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mBackgroundLeash == null) { mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, - RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); + RESIZING_BACKGROUND_SURFACE_NAME); t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } @@ -248,7 +244,7 @@ public class SplitDecorManager extends WindowlessWindowManager { final int left = isLandscape ? mOldMainBounds.width() : 0; final int top = isLandscape ? 0 : mOldMainBounds.height(); mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, - GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession); + GAP_BACKGROUND_SURFACE_NAME); // Fill up another side bounds area. t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask)) .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2) @@ -405,7 +401,7 @@ public class SplitDecorManager extends WindowlessWindowManager { if (mBackgroundLeash == null) { // Initialize background mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, - RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); + RESIZING_BACKGROUND_SURFACE_NAME); t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 46c1a43f9efe..c5f19742c803 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -36,7 +36,6 @@ import android.view.InsetsState; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -98,7 +97,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(TAG) .setHidden(true) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index 0564c95aef5c..d2b4f1ab6b0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -38,7 +38,6 @@ import android.util.Log; import android.view.IWindow; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -173,7 +172,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { String className = getClass().getSimpleName(); - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName(className + "Leash") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt index 831b331a11e9..abc26cfb3e13 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt @@ -24,7 +24,6 @@ import android.os.Binder import android.view.IWindow import android.view.SurfaceControl import android.view.SurfaceControlViewHost -import android.view.SurfaceSession import android.view.View import android.view.WindowManager import android.view.WindowlessWindowManager @@ -106,7 +105,7 @@ class CompatUIComponent( attrs: WindowManager.LayoutParams ): SurfaceControl? { val className = javaClass.simpleName - val builder = SurfaceControl.Builder(SurfaceSession()) + val builder = SurfaceControl.Builder() .setContainerLayer() .setName(className + "Leash") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 02ecfd983d73..7054c17cfeb0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -38,6 +38,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; import com.android.wm.shell.bubbles.BubbleDataRepository; @@ -240,6 +241,7 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { @@ -263,6 +265,7 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser, + assistContentRequester, multiInstanceHelper, desktopTasksLimiter, desktopActivityOrientationHandler); @@ -291,6 +294,15 @@ public abstract class WMShellModule { return new AppToWebGenericLinksParser(context, mainExecutor); } + @Provides + static AssistContentRequester provideAssistContentRequester( + Context context, + @ShellMainThread ShellExecutor shellExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor + ) { + return new AssistContentRequester(context, shellExecutor, bgExecutor); + } + // // Freeform // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index bfc0ee803591..72619195fb3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -45,6 +45,7 @@ import android.view.animation.DecelerateInterpolator; import androidx.annotation.VisibleForTesting; +import com.android.internal.policy.SystemBarUtils; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; @@ -173,8 +174,7 @@ public class DesktopModeVisualIndicator { final Region region = new Region(); int transitionHeight = mDragStartState == DragStartState.FROM_FREEFORM || mDragStartState == DragStartState.DRAGGED_INTENT - ? mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness) + ? SystemBarUtils.getStatusBarHeight(mContext) : 2 * layout.stableInsets().top; // A Rect at the top of the screen that takes up the center 40%. if (mDragStartState == DragStartState.FROM_FREEFORM) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java index 71cc8df80cad..422656c6d387 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -38,7 +38,6 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -105,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setColorLayer() .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) .setFormat(PixelFormat.RGB_888) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4df649ca8c93..f060158766fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -497,13 +497,8 @@ public class PipAnimationController { mCurrentValue = value; } - boolean shouldApplyCornerRadius() { - return !isOutPipDirection(mTransitionDirection); - } - boolean shouldApplyShadowRadius() { - return !isOutPipDirection(mTransitionDirection) - && !isRemovePipDirection(mTransitionDirection); + return !isRemovePipDirection(mTransitionDirection); } boolean inScaleTransition() { @@ -556,7 +551,7 @@ public class PipAnimationController { final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; setCurrentValue(alpha); getSurfaceTransactionHelper().alpha(tx, leash, alpha) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) { tx.apply(); @@ -572,7 +567,7 @@ public class PipAnimationController { getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); @@ -686,13 +681,11 @@ public class PipAnimationController { getSurfaceTransactionHelper().scaleAndCrop(tx, leash, adjustedSourceRectHint, initialSourceValue, bounds, insets, isInPipDirection, fraction); - if (shouldApplyCornerRadius()) { - final Rect sourceBounds = new Rect(initialContainerRect); - sourceBounds.inset(insets); - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + final Rect sourceBounds = new Rect(initialContainerRect); + sourceBounds.inset(insets); + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); } if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) { tx.apply(); @@ -741,11 +734,9 @@ public class PipAnimationController { .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, rotationDelta == ROTATION_270 /* clockwise */); - if (shouldApplyCornerRadius()) { - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) { tx.apply(); } @@ -761,7 +752,7 @@ public class PipAnimationController { void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() .alpha(tx, leash, 1f) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java deleted file mode 100644 index 1dad4137a9df..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; - -import android.content.Context; -import android.view.SurfaceSession; -import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; - -import com.android.internal.protolog.ProtoLog; -import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.windowdecor.WindowDecorViewModel; - -import java.util.Optional; - -/** - * Main stage for split-screen mode. When split-screen is active all standard activity types launch - * on the main stage, except for task that are explicitly pinned to the {@link StageTaskListener}. - * @see StageCoordinator - */ -class MainStage extends StageTaskListener { - private boolean mIsActive = false; - - MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, - StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, - Optional<WindowDecorViewModel> windowDecorViewModel) { - super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, - iconProvider, windowDecorViewModel); - } - - boolean isActive() { - return mIsActive; - } - - void activate(WindowContainerTransaction wct, boolean includingTopTask) { - if (mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: main stage includingTopTask=%b", - includingTopTask); - - if (includingTopTask) { - reparentTopTask(wct); - } - - mIsActive = true; - } - - void deactivate(WindowContainerTransaction wct) { - deactivate(wct, false /* toTop */); - } - - void deactivate(WindowContainerTransaction wct, boolean toTop) { - if (!mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: main stage toTop=%b rootTaskInfo=%s", - toTop, mRootTaskInfo); - mIsActive = false; - - if (mRootTaskInfo == null) return; - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.reparentTasks( - rootToken, - null /* newParent */, - null /* windowingModes */, - null /* activityTypes */, - toTop); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 526c1d4a179d..b36b1f84d21f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -44,7 +44,7 @@ public interface SplitScreen { int STAGE_TYPE_UNDEFINED = -1; /** * The main stage type. - * @see MainStage + * @see StageTaskListener */ int STAGE_TYPE_MAIN = 0; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 7e165afce7d4..793e2aa757a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -61,7 +61,6 @@ import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.widget.Toast; import android.window.RemoteTransition; @@ -897,7 +896,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps, SurfaceControl.Transaction t, String callsite) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName("RecentsAnimationSplitTasks") .setHidden(false) 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 f3959cca050b..dad0d4eb4d8d 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 @@ -34,6 +34,7 @@ import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; @@ -103,7 +104,6 @@ import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.widget.Toast; import android.window.DisplayAreaInfo; @@ -154,14 +154,12 @@ import java.util.Set; import java.util.concurrent.Executor; /** - * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and - * other stages. + * Coordinates the staging (visibility, sizing, ...) of the split-screen stages. * Some high-level rules: * - The {@link StageCoordinator} is only considered active if the other stages contain at * least one child task. - * - The {@link MainStage} should only have children if the coordinator is active. - * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} - * and other stages are visible. + * - The {@link SplitLayout} divider is only visible if multiple {@link StageTaskListener}s are + * visible * - Both stages are put under a single-top root task. * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} @@ -172,9 +170,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private static final String TAG = StageCoordinator.class.getSimpleName(); - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - - private final MainStage mMainStage; + private final StageTaskListener mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); private final StageTaskListener mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); @@ -329,13 +325,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task"); - mMainStage = new MainStage( + mMainStage = new StageTaskListener( mContext, mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); mSideStage = new StageTaskListener( @@ -344,7 +339,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayId, mSideStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); mDisplayController = displayController; @@ -367,8 +361,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, MainStage mainStage, StageTaskListener sideStage, - DisplayController displayController, DisplayImeController displayImeController, + ShellTaskOrganizer taskOrganizer, StageTaskListener mainStage, + StageTaskListener sideStage, DisplayController displayController, + DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, @@ -420,6 +415,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSideStageListener.mVisible && mMainStageListener.mVisible; } + private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) { + mMainStage.activate(wct, includingTopTask); + } + public boolean isSplitActive() { return mMainStage.isActive(); } @@ -505,10 +504,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "removeFromSideStage: task=%d", taskId); final WindowContainerTransaction wct = new WindowContainerTransaction(); - /** - * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the - * other stages no longer have children. - */ + + // MainStage will be deactivated in onStageHasChildrenChanged() if the other stages + // no longer have children. + final boolean result = mSideStage.removeTask(taskId, isSplitActive() ? mMainStage.mRootTaskInfo.token : null, wct); @@ -805,7 +804,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(wct, false /* reparent */); + activateSplit(wct, false /* reparentToTop */); } mSplitLayout.setDivideRatio(snapPosition); updateWindowBounds(mSplitLayout, wct); @@ -872,7 +871,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(wct, false /* reparent */); + activateSplit(wct, false /* reparentToTop */); } setSideStagePosition(splitPosition, wct); @@ -1439,7 +1438,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); } - mMainStage.activate(wct, true /* includingTopTask */); + activateSplit(wct, true /* reparentToTop */); prepareSplitLayout(wct, resizeAnim); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 459355305280..d64c0a24be68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -39,7 +39,6 @@ import android.util.Slog; import android.util.SparseArray; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -72,6 +71,10 @@ import java.util.function.Predicate; public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private static final String TAG = StageTaskListener.class.getSimpleName(); + // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the + // stages should have this be set/being used + private boolean mIsActive; + /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); @@ -89,7 +92,6 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private final Context mContext; private final StageListenerCallbacks mCallbacks; - private final SurfaceSession mSurfaceSession; private final SyncTransactionQueue mSyncQueue; private final IconProvider mIconProvider; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; @@ -104,12 +106,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession, IconProvider iconProvider, + IconProvider iconProvider, Optional<WindowDecorViewModel> windowDecorViewModel) { mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; - mSurfaceSession = surfaceSession; mIconProvider = iconProvider; mWindowDecorViewModel = windowDecorViewModel; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); @@ -199,12 +200,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { mRootTaskInfo = taskInfo; mSplitDecorManager = new SplitDecorManager( mRootTaskInfo.configuration, - mIconProvider, - mSurfaceSession); + mIconProvider); mCallbacks.onRootTaskAppeared(); sendStatusChanged(); mSyncQueue.runInSync(t -> mDimLayer = - SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession)); + SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer")); } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { final int taskId = taskInfo.taskId; mChildrenLeashes.put(taskId, leash); @@ -475,6 +475,44 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { }); } + // --------- + // Previously only used in MainStage + boolean isActive() { + return mIsActive; + } + + void activate(WindowContainerTransaction wct, boolean includingTopTask) { + if (mIsActive) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b", + includingTopTask); + + if (includingTopTask) { + reparentTopTask(wct); + } + + mIsActive = true; + } + + void deactivate(WindowContainerTransaction wct) { + deactivate(wct, false /* toTop */); + } + + void deactivate(WindowContainerTransaction wct, boolean toTop) { + if (!mIsActive) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: toTop=%b rootTaskInfo=%s", + toTop, mRootTaskInfo); + mIsActive = false; + + if (mRootTaskInfo == null) return; + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.reparentTasks( + rootToken, + null /* newParent */, + null /* windowingModes */, + null /* activityTypes */, + toTop); + } + // -------- // Previously only used in SideStage boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { 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 fac3592896ea..2e9b53eee13f 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 @@ -33,7 +33,6 @@ import android.hardware.display.DisplayManager; import android.util.SparseArray; import android.view.IWindow; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.window.SplashScreenView; @@ -204,7 +203,7 @@ public class StartingSurfaceDrawer { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setContainerLayer() .setName("Windowless window") .setHidden(false) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 4fc6c4489f2b..ff4b981f5e8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -92,7 +92,6 @@ import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; @@ -134,8 +133,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private final TransitionAnimation mTransitionAnimation; private final DevicePolicyManager mDevicePolicyManager; - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - /** Keeps track of the currently-running animations associated with each transition. */ private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); @@ -705,7 +702,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { TransitionInfo.Change change, TransitionInfo info, int animHint, ArrayList<Animator> animations, Runnable onAnimFinish) { final int rootIdx = TransitionUtil.rootIndexFor(change, info); - final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession, + final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(), animHint); // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real @@ -918,7 +915,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + final WindowThumbnail wt = WindowThumbnail.createAndAttach( change.getLeash(), thumbnail, transaction); final Animation a = mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds); @@ -943,7 +940,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Runnable finishCallback, TransitionInfo.Change change, TransitionInfo.AnimationOptions options, float cornerRadius) { final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); - final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + final WindowThumbnail wt = WindowThumbnail.createAndAttach( change.getLeash(), options.getThumbnail(), transaction); final Rect bounds = change.getEndAbsBounds(); final int orientation = mContext.getResources().getConfiguration().orientation; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index 3d79a1c8cebe..c385f9afcf3a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -74,8 +74,12 @@ public class HomeTransitionObserver implements TransitionObserver, final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED); if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { if (Flags.migratePredictiveBackTransition()) { - if (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode)) { - notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode)); + final boolean gestureToHomeTransition = isBackGesture + && TransitionUtil.isClosingType(info.getType()); + if (gestureToHomeTransition + || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) { + notifyHomeVisibilityChanged(gestureToHomeTransition + || TransitionUtil.isOpeningType(mode)); } } else { if (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java index 0bf9d368ab74..5802e2ca8133 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -38,7 +38,6 @@ import android.util.Slog; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; -import android.view.SurfaceSession; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.window.ScreenCapture; @@ -112,7 +111,7 @@ class ScreenRotationAnimation { /** Intensity of light/whiteness of the layout after rotation occurs. */ private float mEndLuma; - ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool, + ScreenRotationAnimation(Context context, TransactionPool pool, Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) { mContext = context; mTransactionPool = pool; @@ -126,7 +125,7 @@ class ScreenRotationAnimation { mStartRotation = change.getStartRotation(); mEndRotation = change.getEndRotation(); - mAnimLeash = new SurfaceControl.Builder(session) + mAnimLeash = new SurfaceControl.Builder() .setParent(rootLeash) .setEffectLayer() .setCallsite("ShellRotationAnimation") @@ -153,7 +152,7 @@ class ScreenRotationAnimation { return; } - mScreenshotLayer = new SurfaceControl.Builder(session) + mScreenshotLayer = new SurfaceControl.Builder() .setParent(mAnimLeash) .setBLASTLayer() .setSecure(screenshotBuffer.containsSecureLayers()) @@ -178,7 +177,7 @@ class ScreenRotationAnimation { t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight)); if (!isCustomRotate()) { - mBackColorSurface = new SurfaceControl.Builder(session) + mBackColorSurface = new SurfaceControl.Builder() .setParent(rootLeash) .setColorLayer() .setOpaque(true) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java index 2c668ed3d84d..341f2bc66716 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java @@ -21,7 +21,6 @@ import android.graphics.GraphicBuffer; import android.graphics.PixelFormat; import android.hardware.HardwareBuffer; import android.view.SurfaceControl; -import android.view.SurfaceSession; /** * Represents a surface that is displayed over a transition surface. @@ -33,10 +32,10 @@ class WindowThumbnail { private WindowThumbnail() {} /** Create a thumbnail surface and attach it over a parent surface. */ - static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent, + static WindowThumbnail createAndAttach(SurfaceControl parent, HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) { WindowThumbnail windowThumbnail = new WindowThumbnail(); - windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession) + windowThumbnail.mSurfaceControl = new SurfaceControl.Builder() .setParent(parent) .setName("WindowThumanil : " + parent.toString()) .setCallsite("WindowThumanil") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index c88c1e28b011..79190689adc1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -90,6 +90,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; @@ -182,6 +183,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final Region mExclusionRegion = Region.obtain(); private boolean mInImmersiveMode; private final String mSysUIPackageName; + private final AssistContentRequester mAssistContentRequester; private final DisplayChangeController.OnDisplayChangingListener mOnDisplayChangingListener; private final ISystemGestureExclusionListener mGestureExclusionListener = @@ -217,6 +219,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler @@ -238,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { transitions, desktopTasksController, genericLinksParser, + assistContentRequester, multiInstanceHelper, new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory(), @@ -267,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { Transitions transitions, Optional<DesktopTasksController> desktopTasksController, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory, @@ -304,6 +309,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mActivityOrientationChangeHandler = activityOrientationChangeHandler; + mAssistContentRequester = assistContentRequester; mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { DesktopModeWindowDecoration decoration; RunningTaskInfo taskInfo; @@ -626,7 +632,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(mSplitScreenController); + decoration.createHandleMenu(); } } else if (id == R.id.maximize_window) { // TODO(b/346441962): move click detection logic into the decor's @@ -1270,6 +1276,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue, mRootTaskDisplayAreaOrganizer, mGenericLinksParser, + mAssistContentRequester, mMultiInstanceHelper); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 8a012cd4f6dd..142be91fe942 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -38,6 +38,7 @@ import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.WindowConfiguration.WindowingMode; +import android.app.assist.AssistContent; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -76,6 +77,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AppToWebUtils; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.MultiInstanceHelper; @@ -151,6 +153,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private CharSequence mAppName; private CapturedLink mCapturedLink; private Uri mGenericLink; + private Uri mWebUri; private Consumer<Uri> mOpenInBrowserClickListener; private ExclusionRegionListener mExclusionRegionListener; @@ -159,6 +162,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MaximizeMenuFactory mMaximizeMenuFactory; private final HandleMenuFactory mHandleMenuFactory; private final AppToWebGenericLinksParser mGenericLinksParser; + private final AssistContentRequester mAssistContentRequester; // Hover state for the maximize menu and button. The menu will remain open as long as either of // these is true. See {@link #onMaximizeHoverStateChanged()}. @@ -185,16 +189,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper) { this (context, userContext, displayController, splitScreenController, taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue, - rootTaskDisplayAreaOrganizer, genericLinksParser, SurfaceControl.Builder::new, - SurfaceControl.Transaction::new, WindowContainerTransaction::new, - SurfaceControl::new, new WindowManagerWrapper( + rootTaskDisplayAreaOrganizer, genericLinksParser, assistContentRequester, + SurfaceControl.Builder::new, SurfaceControl.Transaction::new, + WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper( context.getSystemService(WindowManager.class)), - new SurfaceControlViewHostFactory() {}, - DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE, - multiInstanceHelper); + new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE, + DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper); } DesktopModeWindowDecoration( @@ -211,6 +215,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, @@ -231,6 +236,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mGenericLinksParser = genericLinksParser; + mAssistContentRequester = assistContentRequester; mMaximizeMenuFactory = maximizeMenuFactory; mHandleMenuFactory = handleMenuFactory; mMultiInstanceHelper = multiInstanceHelper; @@ -489,6 +495,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // Otherwise, return the generic link which is set to null if a generic link is unavailable. if (mCapturedLink != null && !mCapturedLink.mExpired) { return mCapturedLink.mUri; + } else if (mWebUri != null) { + return mWebUri; } return mGenericLink; } @@ -994,18 +1002,32 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Create and display handle menu window. + * Updates app info and creates and displays handle menu window. */ - void createHandleMenu(SplitScreenController splitScreenController) { + void createHandleMenu() { + // Requests assist content. When content is received, calls {@link #onAssistContentReceived} + // which sets app info and creates the handle menu. + mAssistContentRequester.requestAssistContent( + mTaskInfo.taskId, this::onAssistContentReceived); + } + + /** + * Called when assist content is received. updates the saved links and creates the handle menu. + */ + @VisibleForTesting + void onAssistContentReceived(@Nullable AssistContent assistContent) { + mWebUri = assistContent == null ? null : assistContent.getWebUri(); loadAppInfoIfNeeded(); updateGenericLink(); + + // Create and display handle menu mHandleMenu = mHandleMenuFactory.create( this, mWindowManagerWrapper, mRelayoutParams.mLayoutResId, mAppIconBitmap, mAppName, - splitScreenController, + mSplitScreenController, DesktopModeStatus.canEnterDesktopMode(mContext), Flags.enableDesktopWindowingMultiInstanceFeatures() && mMultiInstanceHelper @@ -1019,6 +1041,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandleMenu.show( /* onToDesktopClickListener= */ () -> { mOnToDesktopClickListener.accept(APP_HANDLE_MENU_BUTTON); + mOnToDesktopClickListener.accept(APP_HANDLE_MENU_BUTTON); return Unit.INSTANCE; }, /* onToFullscreenClickListener= */ mOnToFullscreenClickListener, @@ -1340,6 +1363,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, AppToWebGenericLinksParser genericLinksParser, + AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper) { return new DesktopModeWindowDecoration( context, @@ -1355,6 +1379,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin syncQueue, rootTaskDisplayAreaOrganizer, genericLinksParser, + assistContentRequester, multiInstanceHelper); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt index fd6c4d8e604d..fb81ed4169ff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt @@ -30,7 +30,6 @@ import android.view.Display import android.view.LayoutInflater import android.view.SurfaceControl import android.view.SurfaceControlViewHost -import android.view.SurfaceSession import android.view.WindowManager import android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL import android.view.WindowlessWindowManager @@ -66,7 +65,6 @@ class ResizeVeil @JvmOverloads constructor( private val lightColors = dynamicLightColorScheme(context) private val darkColors = dynamicDarkColorScheme(context) - private val surfaceSession = SurfaceSession() private lateinit var iconView: ImageView private var iconSize = 0 @@ -126,7 +124,7 @@ class ResizeVeil @JvmOverloads constructor( .setCallsite("ResizeVeil#setupResizeVeil") .build() backgroundSurface = surfaceControlBuilderFactory - .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession) + .create("Resize veil background of Task=" + taskInfo.taskId) .setColorLayer() .setHidden(true) .setParent(veilSurface) @@ -399,10 +397,6 @@ class ResizeVeil @JvmOverloads constructor( fun create(name: String): SurfaceControl.Builder { return SurfaceControl.Builder().setName(name) } - - fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder { - return SurfaceControl.Builder(surfaceSession).setName(name) - } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index dfa5ab415992..9ef4b8cde8ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -29,6 +29,7 @@ import android.view.View.OnClickListener import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.view.WindowManager import android.widget.ImageButton +import com.android.internal.policy.SystemBarUtils import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators @@ -74,7 +75,10 @@ internal class AppHandleViewHolder( ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) this.taskInfo = taskInfo - if (!isCaptionVisible && hasStatusBarInputLayer()) { + // If handle is not in status bar region(i.e., bottom stage in vertical split), + // do not create an input layer + if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return + if (!isCaptionVisible && hasStatusBarInputLayer() ) { disposeStatusBarInputLayer() return } @@ -120,7 +124,7 @@ internal class AppHandleViewHolder( inputManager.pilferPointers(v.viewRootImpl.inputToken) } captionHandle.dispatchTouchEvent(event) - true + return@setOnTouchListener true } windowManagerWrapper.updateViewLayout(view, lp) } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index ec1d4f7854fd..7640cb1fb616 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -195,6 +195,25 @@ class DesktopModeFlickerScenarios { .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) + val CORNER_RESIZE_TO_MAXIMUM_SIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE_TO_MAXIMUM_SIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ) + .build(), + assertions = + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppLayerIncreasesInSize(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), + AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + val SNAP_RESIZE_LEFT_WITH_BUTTON = FlickerConfigEntry( scenarioId = ScenarioId("SNAP_RESIZE_LEFT_WITH_BUTTON"), diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt new file mode 100644 index 000000000000..0b98ba2a9cd4 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizeLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MAXIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the greatest possible height and width in + * landscape mode. + * + * Assert that the maximum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMaximumWindowSizeLandscape : ResizeAppWithCornerResize( + rotation = Rotation.ROTATION_90 +) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MAXIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResizeToMaximumSize() = + super.resizeAppWithCornerResizeToMaximumSize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MAXIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt new file mode 100644 index 000000000000..b1c04d38a46c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/ResizeAppToMaximumWindowSizePortrait.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MAXIMUM_SIZE +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the greatest possible height and width in + * portrait mode. + * + * Assert that the maximum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMaximumWindowSizePortrait : ResizeAppWithCornerResize() { + @ExpectedScenarios(["CORNER_RESIZE_TO_MAXIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResizeToMaximumSize() = + super.resizeAppWithCornerResizeToMaximumSize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MAXIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt new file mode 100644 index 000000000000..a4dc52beb85d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/CloseAllAppsWithAppHeaderExitTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [CloseAllAppsWithAppHeaderExit]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class CloseAllAppsWithAppHeaderExitTest() : CloseAllAppsWithAppHeaderExit() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt new file mode 100644 index 000000000000..3d95f97c09ef --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.EnterDesktopWithDrag +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [EnterDesktopWithDrag]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class EnterDesktopWithDragTest : EnterDesktopWithDrag() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt new file mode 100644 index 000000000000..140c5ec15812 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ExitDesktopWithDragToTopDragZoneTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ExitDesktopWithDragToTopDragZone +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ExitDesktopWithDragToTopDragZone]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ExitDesktopWithDragToTopDragZoneTest : ExitDesktopWithDragToTopDragZone() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt new file mode 100644 index 000000000000..3d3dcd09cc63 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MaximizeAppWindowTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.MaximizeAppWindow +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [MaximizeAppWindow]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class MaximizeAppWindowTest : MaximizeAppWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt new file mode 100644 index 000000000000..263e89f69e5a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppsInDesktopModeTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.OpenAppsInDesktopMode +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [OpenAppsInDesktopMode]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class OpenAppsInDesktopModeTest : OpenAppsInDesktopMode() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt new file mode 100644 index 000000000000..13f4775c1074 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowAndPipTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppCornerMultiWindowAndPip +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppCornerMultiWindowAndPip]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppCornerMultiWindowAndPipTest : ResizeAppCornerMultiWindowAndPip() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt new file mode 100644 index 000000000000..bc9bb41bf320 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppCornerMultiWindowTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppCornerMultiWindow +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppCornerMultiWindow]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppCornerMultiWindowTest : ResizeAppCornerMultiWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt new file mode 100644 index 000000000000..46168eb7c002 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithCornerResizeTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.ResizeAppWithCornerResize +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppWithCornerResize]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppWithCornerResizeTest : ResizeAppWithCornerResize() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt new file mode 100644 index 000000000000..ee2420021339 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/ResizeAppWithEdgeResizeTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.server.wm.flicker.helpers.MotionEventHelper +import com.android.wm.shell.scenarios.ResizeAppWithEdgeResize +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [ResizeAppWithEdgeResize]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class ResizeAppWithEdgeResizeTest : + ResizeAppWithEdgeResize(MotionEventHelper.InputMethod.TOUCHPAD) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt new file mode 100644 index 000000000000..38e85c755481 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithButtonTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithButton +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SnapResizeAppWindowWithButton]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SnapResizeAppWindowWithButtonTest : SnapResizeAppWindowWithButton() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt new file mode 100644 index 000000000000..082a3fb0e171 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SnapResizeAppWindowWithDragTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SnapResizeAppWindowWithDrag +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SnapResizeAppWindowWithDrag]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SnapResizeAppWindowWithDragTest : SnapResizeAppWindowWithDrag() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt new file mode 100644 index 000000000000..fdd0d8144130 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/SwitchToOverviewFromDesktopTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open 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.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.SwitchToOverviewFromDesktop +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [SwitchToOverviewFromDesktop]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class SwitchToOverviewFromDesktopTest : SwitchToOverviewFromDesktop() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt index e9056f3c44d4..351a70094654 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -33,15 +32,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class CloseAllAppsWithAppHeaderExit -@JvmOverloads +@Ignore("Base Test Class") +abstract class CloseAllAppsWithAppHeaderExit constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt index ca1dc1a7f658..3f9927f1fab6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.MailAppHelper @@ -26,13 +25,11 @@ import com.android.window.flags.Flags import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase() +@Ignore("Test Base Class") +abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase() { private val imeAppHelper = ImeAppHelper(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt index f4d641411114..967bd29958c2 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation import android.tools.flicker.rules.ChangeDisplayOrientationRule @@ -25,19 +24,16 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class EnterDesktopWithDrag -@JvmOverloads +@Ignore("Test Base Class") +abstract class EnterDesktopWithDrag constructor( val rotation: Rotation = Rotation.ROTATION_0, isResizeable: Boolean = true, - isLandscapeApp: Boolean = true + isLandscapeApp: Boolean = true, ) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt index b616e5347ac9..824c4482c1e6 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation import com.android.window.flags.Flags @@ -24,19 +23,16 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class ExitDesktopWithDragToTopDragZone -@JvmOverloads +@Ignore("Test Base Class") +abstract class ExitDesktopWithDragToTopDragZone constructor( val rotation: Rotation = Rotation.ROTATION_0, isResizeable: Boolean = true, - isLandscapeApp: Boolean = true + isLandscapeApp: Boolean = true, ) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index 426f40b5e81b..a54d497bf511 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation -import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation import android.tools.flicker.rules.ChangeDisplayOrientationRule @@ -33,15 +32,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class MaximizeAppWindow -@JvmOverloads +@Ignore("Test Base Class") +abstract class MaximizeAppWindow constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt index a5c794b06c4a..aad266fb8374 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation -import android.platform.test.annotations.Postsubmit import android.tools.flicker.rules.ChangeDisplayOrientationRule import android.tools.NavBar import android.tools.Rotation @@ -36,14 +35,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class OpenAppsInDesktopMode(val rotation: Rotation = Rotation.ROTATION_0) { +@Ignore("Test Base Class") +abstract class OpenAppsInDesktopMode(val rotation: Rotation = Rotation.ROTATION_0) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -83,4 +80,4 @@ open class OpenAppsInDesktopMode(val rotation: Rotation = Rotation.ROTATION_0) { secondApp.exit(wmHelper) firstApp.exit(wmHelper) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt index b6bca7a94287..bfee3181cbc0 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -34,15 +33,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class ResizeAppCornerMultiWindow -@JvmOverloads +@Ignore("Test Base Class") +abstract class ResizeAppCornerMultiWindow constructor(val rotation: Rotation = Rotation.ROTATION_0, val horizontalChange: Int = 50, val verticalChange: Int = -50) { diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt index 285ea13bb9d4..5b1b64e7c562 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -35,15 +34,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class ResizeAppCornerMultiWindowAndPip -@JvmOverloads +@Ignore("Test Base Class") +abstract class ResizeAppCornerMultiWindowAndPip constructor(val rotation: Rotation = Rotation.ROTATION_0, val horizontalChange: Int = 50, val verticalChange: Int = -50) { diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt index 42940a99b59f..bd25639466a3 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -32,16 +31,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class ResizeAppWithCornerResize -@JvmOverloads -constructor( +@Ignore("Test Base Class") +abstract class ResizeAppWithCornerResize( val rotation: Rotation = Rotation.ROTATION_0, val horizontalChange: Int = 200, val verticalChange: Int = -200, @@ -83,6 +78,25 @@ constructor( ) } + @Test + open fun resizeAppWithCornerResizeToMaximumSize() { + val maxResizeChange = 3000 + testApp.cornerResize( + wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + maxResizeChange, + -maxResizeChange + ) + testApp.cornerResize( + wmHelper, + device, + DesktopModeAppHelper.Corners.LEFT_BOTTOM, + -maxResizeChange, + maxResizeChange + ) + } + @After fun teardown() { testApp.exit(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt index d094967e91e0..67802387b267 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -32,15 +31,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class ResizeAppWithEdgeResize -@JvmOverloads +@Ignore("Test Base Class") +abstract class ResizeAppWithEdgeResize constructor( val inputMethod: MotionEventHelper.InputMethod, val rotation: Rotation = Rotation.ROTATION_90 diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt index 33242db66f9f..2b40497844ef 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation -import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation import android.tools.traces.parsers.WindowManagerStateHelper @@ -32,15 +31,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class SnapResizeAppWindowWithButton -@JvmOverloads +@Ignore("Test Base Class") +abstract class SnapResizeAppWindowWithButton constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt index 14eb779165bb..b4bd7e1c5211 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation -import android.platform.test.annotations.Postsubmit import android.tools.NavBar import android.tools.Rotation import android.tools.traces.parsers.WindowManagerStateHelper @@ -32,15 +31,12 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class SnapResizeAppWindowWithDrag -@JvmOverloads +@Ignore("Test Base Class") +abstract class SnapResizeAppWindowWithDrag constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() @@ -72,4 +68,4 @@ constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { fun teardown() { testApp.exit(wmHelper) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt index 53e36e23fd95..dad2eb633c72 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.scenarios -import android.platform.test.annotations.Postsubmit import android.app.Instrumentation import android.tools.NavBar import android.tools.Rotation @@ -31,20 +30,17 @@ import com.android.wm.shell.Utils import org.junit.After import org.junit.Assume import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.BlockJUnit4ClassRunner /** * Base test for opening recent apps overview from desktop mode. * * Navigation mode can be passed as a constructor parameter, by default it is set to gesture navigation. */ -@RunWith(BlockJUnit4ClassRunner::class) -@Postsubmit -open class SwitchToOverviewFromDesktop -@JvmOverloads +@Ignore("Base Test Class") +abstract class SwitchToOverviewFromDesktop constructor(val navigationMode: NavBar = NavBar.MODE_GESTURAL) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 413e49562435..e514dc38208e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -49,7 +49,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.SparseArray; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; @@ -169,7 +168,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase { public void testTaskLeashReleasedAfterVanished() throws RemoteException { assumeFalse(ENABLE_SHELL_TRANSITIONS); RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW); - SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession()) + SurfaceControl taskLeash = new SurfaceControl.Builder() .setName("task").build(); mOrganizer.registerOrganizer(); mOrganizer.onTaskAppeared(taskInfo, taskLeash); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index f558e87c4ed7..2b7f86f36477 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -22,6 +22,7 @@ import android.graphics.Region import android.testing.AndroidTestingRunner import android.view.SurfaceControl import androidx.test.filters.SmallTest +import com.android.internal.policy.SystemBarUtils import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase @@ -67,8 +68,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) - val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_transition_region_thickness) + val transitionHeight = SystemBarUtils.getStatusBarHeight(context) val toFullscreenScale = mContext.resources.getFloat( R.dimen.desktop_mode_fullscreen_region_scale ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java deleted file mode 100644 index b1befc46f383..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.splitscreen; - -import static android.view.Display.DEFAULT_DISPLAY; - -import static com.google.common.truth.Truth.assertThat; - -import android.app.ActivityManager; -import android.view.SurfaceControl; -import android.view.SurfaceSession; -import android.window.WindowContainerTransaction; - -import androidx.test.annotation.UiThreadTest; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.launcher3.icons.IconProvider; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.TestRunningTaskInfoBuilder; -import com.android.wm.shell.common.SyncTransactionQueue; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Optional; - -/** Tests for {@link MainStage} */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class MainStageTests extends ShellTestCase { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private ActivityManager.RunningTaskInfo mRootTaskInfo; - @Mock private SurfaceControl mRootLeash; - @Mock private IconProvider mIconProvider; - private WindowContainerTransaction mWct = new WindowContainerTransaction(); - private SurfaceSession mSurfaceSession = new SurfaceSession(); - private MainStage mMainStage; - - @Before - @UiThreadTest - public void setup() { - MockitoAnnotations.initMocks(this); - mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); - mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, - mSyncQueue, mSurfaceSession, mIconProvider, Optional.empty()); - mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); - } - - @Test - public void testActiveDeactivate() { - mMainStage.activate(mWct, true /* reparent */); - assertThat(mMainStage.isActive()).isTrue(); - - mMainStage.deactivate(mWct); - assertThat(mMainStage.isActive()).isFalse(); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index 4de227836104..66dcef6f14cc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -24,7 +24,6 @@ import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.view.SurfaceControl; -import android.view.SurfaceSession; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.wm.shell.ShellTaskOrganizer; @@ -74,10 +73,10 @@ public class SplitTestUtils { final SurfaceControl mRootLeash; TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, - ShellTaskOrganizer taskOrganizer, MainStage mainStage, StageTaskListener sideStage, - DisplayController displayController, DisplayImeController imeController, - DisplayInsetsController insetsController, SplitLayout splitLayout, - Transitions transitions, TransactionPool transactionPool, + ShellTaskOrganizer taskOrganizer, StageTaskListener mainStage, + StageTaskListener sideStage, DisplayController displayController, + DisplayImeController imeController, DisplayInsetsController insetsController, + SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, Handler mainHandler, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, @@ -89,7 +88,7 @@ public class SplitTestUtils { // Prepare root task for testing. mRootTask = new TestRunningTaskInfoBuilder().build(); - mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build(); + mRootLeash = new SurfaceControl.Builder().setName("test").build(); onTaskAppeared(mRootTask, mRootLeash); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index e16743386489..ce3944a5855e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -53,7 +53,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.IRemoteTransition; import android.window.RemoteTransition; import android.window.TransitionInfo; @@ -106,7 +105,6 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; @Mock private Transitions mTransitions; - @Mock private SurfaceSession mSurfaceSession; @Mock private IconProvider mIconProvider; @Mock private WindowDecorViewModel mWindowDecorViewModel; @Mock private ShellExecutor mMainExecutor; @@ -116,7 +114,7 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private SplitScreen.SplitInvocationListener mInvocationListener; private final TestShellExecutor mTestShellExecutor = new TestShellExecutor(); private SplitLayout mSplitLayout; - private MainStage mMainStage; + private StageTaskListener mMainStage; private StageTaskListener mSideStage; private StageCoordinator mStageCoordinator; private SplitScreenTransitions mSplitScreenTransitions; @@ -133,12 +131,12 @@ public class SplitTransitionTests extends ShellTestCase { doReturn(mockExecutor).when(mTransitions).getAnimExecutor(); doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire(); mSplitLayout = SplitTestUtils.createMockSplitLayout(); - mMainStage = spy(new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mIconProvider, Optional.of(mWindowDecorViewModel))); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mIconProvider, Optional.of(mWindowDecorViewModel))); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index c9e1414c39b9..a6c16c43c8cb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -50,7 +50,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.RemoteTransition; import android.window.WindowContainerTransaction; @@ -97,7 +96,7 @@ public class StageCoordinatorTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock - private MainStage mMainStage; + private StageTaskListener mMainStage; @Mock private StageTaskListener mSideStage; @Mock @@ -119,7 +118,6 @@ public class StageCoordinatorTests extends ShellTestCase { private final Rect mBounds2 = new Rect(5, 10, 15, 20); private final Rect mRootBounds = new Rect(0, 0, 45, 60); - private SurfaceSession mSurfaceSession = new SurfaceSession(); private SurfaceControl mRootLeash; private SurfaceControl mDividerLeash; private ActivityManager.RunningTaskInfo mRootTask; @@ -139,7 +137,7 @@ public class StageCoordinatorTests extends ShellTestCase { mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty())); - mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build(); + mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); when(mSplitLayout.getBounds1()).thenReturn(mBounds1); when(mSplitLayout.getBounds2()).thenReturn(mBounds2); @@ -149,7 +147,7 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash); mRootTask = new TestRunningTaskInfoBuilder().build(); - mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mRootLeash = new SurfaceControl.Builder().setName("test").build(); mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash); mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index acd612eb34d9..b7b7d0d35bcf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -32,7 +32,6 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.os.SystemProperties; import android.view.SurfaceControl; -import android.view.SurfaceSession; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -82,7 +81,6 @@ public final class StageTaskListenerTests extends ShellTestCase { private WindowContainerTransaction mWct; @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; - private SurfaceSession mSurfaceSession = new SurfaceSession(); private SurfaceControl mSurfaceControl; private ActivityManager.RunningTaskInfo mRootTask; private StageTaskListener mStageTaskListener; @@ -97,12 +95,11 @@ public final class StageTaskListenerTests extends ShellTestCase { DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession, mIconProvider, Optional.of(mWindowDecorViewModel)); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; - mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mSurfaceControl = new SurfaceControl.Builder().setName("test").build(); mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); } @@ -199,4 +196,13 @@ public final class StageTaskListenerTests extends ShellTestCase { assertThat(mStageTaskListener.removeTask(task.taskId, null, mWct)).isTrue(); verify(mWct).reparent(eq(task.token), isNull(), eq(false)); } + + @Test + public void testActiveDeactivate() { + mStageTaskListener.activate(mWct, true /* reparent */); + assertThat(mStageTaskListener.isActive()).isTrue(); + + mStageTaskListener.deactivate(mWct); + assertThat(mStageTaskListener.isActive()).isFalse(); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 198488582700..17fd95b69dba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -49,7 +49,6 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.view.SurfaceHolder; -import android.view.SurfaceSession; import android.view.ViewTreeObserver; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -95,7 +94,6 @@ public class TaskViewTest extends ShellTestCase { Looper mViewLooper; TestHandler mViewHandler; - SurfaceSession mSession; SurfaceControl mLeash; Context mContext; @@ -106,7 +104,7 @@ public class TaskViewTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mLeash = new SurfaceControl.Builder(mSession) + mLeash = new SurfaceControl.Builder() .setName("test") .build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index f51a9608d442..8f49de0a98fb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; @@ -39,6 +40,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; @@ -213,6 +215,35 @@ public class HomeTransitionObserverTest extends ShellTestCase { verify(mListener, times(1)).onHomeVisibilityChanged(true); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION) + public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose() + throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + when(info.getType()).thenReturn(TRANSIT_PREPARE_BACK_NAVIGATION); + + when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); + + when(info.getType()).thenReturn(TRANSIT_TO_BACK); + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true); + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + verify(mListener, times(1)).onHomeVisibilityChanged(true); + } + /** * Helper class to initialize variables for the rest. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index be0549b6655d..3dd8a2bacbcd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -73,6 +73,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser +import com.android.wm.shell.apptoweb.AssistContentRequester import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController @@ -165,6 +166,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser @Mock private lateinit var mockUserHandle: UserHandle + @Mock private lateinit var mockAssistContentRequester: AssistContentRequester @Mock private lateinit var mockToast: Toast private val bgExecutor = TestShellExecutor() @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper @@ -218,6 +220,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockTransitions, Optional.of(mockDesktopTasksController), mockGenericLinksParser, + mockAssistContentRequester, mockMultiInstanceHelper, mockDesktopModeWindowDecorFactory, mockInputMonitorFactory, @@ -1131,7 +1134,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { whenever( mockDesktopModeWindowDecorFactory.create( any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), - any(), any(), any()) + any(), any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.isFocused).thenReturn(task.isFocused) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 258c86094a36..b9e542a0e0c1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -46,6 +46,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.assist.AssistContent; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; @@ -88,6 +89,7 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.apptoweb.AssistContentRequester; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; @@ -133,6 +135,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final Uri TEST_URI1 = Uri.parse("https://www.google.com/"); private static final Uri TEST_URI2 = Uri.parse("https://docs.google.com/"); + private static final Uri TEST_URI3 = Uri.parse("https://slides.google.com/"); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); @@ -175,6 +178,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private WindowManager mMockWindowManager; @Mock + private AssistContentRequester mMockAssistContentRequester; + @Mock private HandleMenu mMockHandleMenu; @Mock private HandleMenuFactory mMockHandleMenuFactory; @@ -189,7 +194,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private SurfaceControl.Transaction mMockTransaction; private StaticMockitoSession mMockitoSession; private TestableContext mTestableContext; - private ShellExecutor mBgExecutor = new TestShellExecutor(); + private final ShellExecutor mBgExecutor = new TestShellExecutor(); + private final AssistContent mAssistContent = new AssistContent(); /** Set up run before test class. */ @BeforeClass @@ -673,10 +679,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_handleMenuBrowserLinkSetToCapturedLinkIfValid() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* generic link */); + taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); // Verify handle menu's browser link set as captured link - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verifyHandleMenuCreated(TEST_URI1); } @@ -685,7 +692,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_postsOnCapturedLinkExpiredRunnable() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, null /* generic link */); + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); // Run runnable to set captured link to expired @@ -694,7 +702,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Verify captured link is no longer valid by verifying link is not set as handle menu // browser link. - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verifyHandleMenuCreated(null /* uri */); } @@ -703,7 +711,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_capturedLinkNotResetToSameLink() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, null /* generic link */); + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); // Run runnable to set captured link to expired @@ -714,7 +723,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { decor.relayout(taskInfo); // Verify handle menu's browser link not set to captured link since link is expired - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verifyHandleMenuCreated(null /* uri */); } @@ -723,11 +732,12 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_capturedLinkStillUsedIfExpiredAfterHandleMenuCreation() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, null /* generic link */); + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); final ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); // Create handle menu before link expires - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); // Run runnable to set captured link to expired verify(mMockHandler).postDelayed(runnableArgument.capture(), anyLong()); @@ -735,7 +745,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Verify handle menu's browser link is set to captured link since menu was opened before // captured link expired - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verifyHandleMenuCreated(TEST_URI1); } @@ -744,12 +754,13 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_capturedLinkExpiresAfterClick() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, null /* generic link */); + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor = ArgumentCaptor.forClass(Function1.class); // Simulate menu opening and clicking open in browser button - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verify(mMockHandleMenu).show( any(), any(), @@ -763,7 +774,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { // Verify handle menu's browser link not set to captured link since link not valid after // open in browser clicked - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verifyHandleMenuCreated(null /* uri */); } @@ -772,10 +783,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { public void capturedLink_openInBrowserListenerCalledOnClick() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, TEST_URI1 /* captured link */, null /* generic link */); + taskInfo, TEST_URI1 /* captured link */, null /* web uri */, + null /* generic link */); final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor = ArgumentCaptor.forClass(Function1.class); - decor.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decor); verify(mMockHandleMenu).show( any(), any(), @@ -793,24 +805,38 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) - public void genericLink_genericLinkUsedWhenCapturedLinkUnavailable() { + public void webUriLink_webUriLinkUsedWhenCapturedLinkUnavailable() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); final DesktopModeWindowDecoration decor = createWindowDecoration( - taskInfo, null /* captured link */, TEST_URI2 /* generic link */); - - // Verify handle menu's browser link set as generic link no captured link is available - decor.createHandleMenu(mMockSplitScreenController); + taskInfo, null /* captured link */, TEST_URI2 /* web uri */, + TEST_URI3 /* generic link */); + // Verify handle menu's browser link set as web uri link when captured link is unavailable + createHandleMenu(decor); verifyHandleMenuCreated(TEST_URI2); } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB) + public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final DesktopModeWindowDecoration decor = createWindowDecoration( + taskInfo, null /* captured link */, null /* web uri */, + TEST_URI3 /* generic link */); + + // Verify handle menu's browser link set as generic link when captured link and web uri link + // are unavailable + createHandleMenu(decor); + verifyHandleMenuCreated(TEST_URI3); + } + + @Test public void handleMenu_onCloseMenuClick_closesMenu() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, true /* relayout */); final ArgumentCaptor<Function0<Unit>> closeClickListener = ArgumentCaptor.forClass(Function0.class); - decoration.createHandleMenu(mMockSplitScreenController); + createHandleMenu(decoration); verify(mMockHandleMenu).show( any(), any(), @@ -860,9 +886,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, @Nullable Uri capturedLink, - @Nullable Uri genericLink) { + @Nullable Uri webUri, @Nullable Uri genericLink) { taskInfo.capturedLink = capturedLink; taskInfo.capturedLinkTimestamp = System.currentTimeMillis(); + mAssistContent.setWebUri(webUri); final String genericLinkString = genericLink == null ? null : genericLink.toString(); doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any()); // Relayout to set captured link @@ -894,11 +921,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { mContext, mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, - mMockGenericLinksParser, SurfaceControl.Builder::new, mMockTransactionSupplier, - WindowContainerTransaction::new, SurfaceControl::new, - new WindowManagerWrapper(mMockWindowManager), - mMockSurfaceControlViewHostFactory, maximizeMenuFactory, mMockHandleMenuFactory, - mMockMultiInstanceHelper); + mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new, + mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, + new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, + maximizeMenuFactory, mMockHandleMenuFactory, mMockMultiInstanceHelper); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -926,6 +952,13 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } + private void createHandleMenu(@NonNull DesktopModeWindowDecoration decor) { + decor.createHandleMenu(); + // Call DesktopModeWindowDecoration#onAssistContentReceived because decor waits to receive + // {@link AssistContent} before creating the menu + decor.onAssistContentReceived(mAssistContent); + } + private static boolean hasNoInputChannelFeature(RelayoutParams params) { return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) != 0; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index a07be79579eb..e0d16aab1e07 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -97,7 +97,7 @@ class ResizeVeilTest : ShellTestCase() { .thenReturn(spyResizeVeilSurfaceBuilder) doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory - .create(eq("Resize veil background of Task=" + taskInfo.taskId), any())) + .create(eq("Resize veil background of Task=" + taskInfo.taskId))) .thenReturn(spyBackgroundSurfaceBuilder) doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp index e6182454ad8a..5955915c9fcd 100644 --- a/libs/androidfw/AssetManager.cpp +++ b/libs/androidfw/AssetManager.cpp @@ -1420,18 +1420,20 @@ void AssetManager::mergeInfoLocked(SortedVector<AssetDir::FileInfo>* pMergedInfo Mutex AssetManager::SharedZip::gLock; DefaultKeyedVector<String8, wp<AssetManager::SharedZip> > AssetManager::SharedZip::gOpen; -AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen) - : mPath(path), mZipFile(NULL), mModWhen(modWhen), - mResourceTableAsset(NULL), mResourceTable(NULL) -{ - if (kIsDebug) { - ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); - } - ALOGV("+++ opening zip '%s'\n", mPath.c_str()); - mZipFile = ZipFileRO::open(mPath.c_str()); - if (mZipFile == NULL) { - ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); - } +AssetManager::SharedZip::SharedZip(const String8& path, ModDate modWhen) + : mPath(path), + mZipFile(NULL), + mModWhen(modWhen), + mResourceTableAsset(NULL), + mResourceTable(NULL) { + if (kIsDebug) { + ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); + } + ALOGV("+++ opening zip '%s'\n", mPath.c_str()); + mZipFile = ZipFileRO::open(mPath.c_str()); + if (mZipFile == NULL) { + ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); + } } AssetManager::SharedZip::SharedZip(int fd, const String8& path) @@ -1453,7 +1455,7 @@ sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path, bool createIfNotPresent) { AutoMutex _l(gLock); - time_t modWhen = getFileModDate(path.c_str()); + auto modWhen = getFileModDate(path.c_str()); sp<SharedZip> zip = gOpen.valueFor(path).promote(); if (zip != NULL && zip->mModWhen == modWhen) { return zip; @@ -1520,8 +1522,8 @@ ResTable* AssetManager::SharedZip::setResourceTable(ResTable* res) bool AssetManager::SharedZip::isUpToDate() { - time_t modWhen = getFileModDate(mPath.c_str()); - return mModWhen == modWhen; + auto modWhen = getFileModDate(mPath.c_str()); + return mModWhen == modWhen; } void AssetManager::SharedZip::addOverlay(const asset_path& ap) diff --git a/libs/androidfw/include/androidfw/AssetManager.h b/libs/androidfw/include/androidfw/AssetManager.h index ce0985b38986..376c881ea376 100644 --- a/libs/androidfw/include/androidfw/AssetManager.h +++ b/libs/androidfw/include/androidfw/AssetManager.h @@ -280,21 +280,21 @@ private: ~SharedZip(); private: - SharedZip(const String8& path, time_t modWhen); - SharedZip(int fd, const String8& path); - SharedZip(); // <-- not implemented + SharedZip(const String8& path, ModDate modWhen); + SharedZip(int fd, const String8& path); + SharedZip(); // <-- not implemented - String8 mPath; - ZipFileRO* mZipFile; - time_t mModWhen; + String8 mPath; + ZipFileRO* mZipFile; + ModDate mModWhen; - Asset* mResourceTableAsset; - ResTable* mResourceTable; + Asset* mResourceTableAsset; + ResTable* mResourceTable; - Vector<asset_path> mOverlays; + Vector<asset_path> mOverlays; - static Mutex gLock; - static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; + static Mutex gLock; + static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; }; /* diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 64b1f0c6ed03..98f1aa86f2db 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -25,8 +25,9 @@ #include "android-base/macros.h" #include "android-base/unique_fd.h" #include "androidfw/ConfigDescription.h" -#include "androidfw/StringPiece.h" #include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" namespace android { @@ -202,7 +203,7 @@ class LoadedIdmap { android::base::unique_fd idmap_fd_; std::string_view overlay_apk_path_; std::string_view target_apk_path_; - time_t idmap_last_mod_time_; + ModDate idmap_last_mod_time_; private: DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index 077609d20d55..09ae40c35369 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once -#include <sys/types.h> +#include <time.h> // // Handy utility functions and portability code. // -#ifndef _LIBS_ANDROID_FW_MISC_H -#define _LIBS_ANDROID_FW_MISC_H namespace android { @@ -41,15 +40,35 @@ typedef enum FileType { } FileType; /* get the file's type; follows symlinks */ FileType getFileType(const char* fileName); -/* get the file's modification date; returns -1 w/errno set on failure */ -time_t getFileModDate(const char* fileName); + +// MinGW doesn't support nanosecond resolution in stat() modification time, and given +// that it only matters on the device it's ok to keep it at the second level there. +#ifdef _WIN32 +using ModDate = time_t; +inline constexpr ModDate kInvalidModDate = ModDate(-1); +inline constexpr unsigned long long kModDateResolutionNs = 1ull * 1000 * 1000 * 1000; +inline time_t toTimeT(ModDate m) { + return m; +} +#else +using ModDate = timespec; +inline constexpr ModDate kInvalidModDate = {-1, -1}; +inline constexpr unsigned long long kModDateResolutionNs = 1; +inline time_t toTimeT(ModDate m) { + return m.tv_sec; +} +#endif + +/* get the file's modification date; returns kInvalidModDate w/errno set on failure */ +ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ -time_t getFileModDate(int fd); +ModDate getFileModDate(int fd); // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); -}; // namespace android +} // namespace android -#endif // _LIBS_ANDROID_FW_MISC_H +// Whoever uses getFileModDate() will need this as well +bool operator==(const timespec& l, const timespec& r); diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 93dcaf549a90..9bdaf18a116a 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -28,11 +28,13 @@ #include <sys/vfs.h> #endif // __linux__ -#include <cstring> -#include <cstdio> #include <errno.h> #include <sys/stat.h> +#include <cstdio> +#include <cstring> +#include <tuple> + namespace android { /* @@ -73,27 +75,32 @@ FileType getFileType(const char* fileName) } } -/* - * Get a file's modification date. - */ -time_t getFileModDate(const char* fileName) { - struct stat sb; - if (stat(fileName, &sb) < 0) { - return (time_t)-1; - } - return sb.st_mtime; +static ModDate getModDate(const struct stat& st) { +#ifdef _WIN32 + return st.st_mtime; +#else + return st.st_mtim; +#endif } -time_t getFileModDate(int fd) { - struct stat sb; - if (fstat(fd, &sb) < 0) { - return (time_t)-1; - } - if (sb.st_nlink <= 0) { - errno = ENOENT; - return (time_t)-1; - } - return sb.st_mtime; +ModDate getFileModDate(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + return kInvalidModDate; + } + return getModDate(sb); +} + +ModDate getFileModDate(int fd) { + struct stat sb; + if (fstat(fd, &sb) < 0) { + return kInvalidModDate; + } + if (sb.st_nlink <= 0) { + errno = ENOENT; + return kInvalidModDate; + } + return getModDate(sb); } #ifndef __linux__ @@ -124,4 +131,8 @@ bool isReadonlyFilesystem(int fd) { } #endif // __linux__ -}; // namespace android +} // namespace android + +bool operator==(const timespec& l, const timespec& r) { + return std::tie(l.tv_sec, l.tv_nsec) == std::tie(r.tv_sec, l.tv_nsec); +} diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 60aa7d88925d..cb2e56f5f5e4 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include <chrono> +#include <thread> + #include "android-base/file.h" #include "androidfw/ApkAssets.h" #include "androidfw/AssetManager2.h" @@ -27,6 +30,7 @@ #include "data/overlayable/R.h" #include "data/system/R.h" +using namespace std::chrono_literals; using ::testing::NotNull; namespace overlay = com::android::overlay; @@ -218,10 +222,13 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { unlink(temp_file.path); ASSERT_FALSE(apk_assets->IsUpToDate()); - sleep(2); + + const auto sleep_duration = + std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); + std::this_thread::sleep_for(sleep_duration); base::WriteStringToFile("hello", temp_file.path); - sleep(2); + std::this_thread::sleep_for(sleep_duration); ASSERT_FALSE(apk_assets->IsUpToDate()); } diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index 09232b64616d..a58493aa47ca 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -7,6 +7,17 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +cc_library_headers { + name: "libhostgraphics_headers", + host_supported: true, + export_include_dirs: ["include"], + target: { + windows: { + enabled: true, + }, + }, +} + cc_library_host_static { name: "libhostgraphics", @@ -30,12 +41,13 @@ cc_library_host_static { ], header_libs: [ + "libhostgraphics_headers", "libnativebase_headers", "libnativedisplay_headers", "libnativewindow_headers", ], - export_include_dirs: ["."], + export_include_dirs: ["include"], target: { windows: { diff --git a/libs/hostgraphics/gui/BufferItem.h b/libs/hostgraphics/include/gui/BufferItem.h index e95a9231dfaf..e95a9231dfaf 100644 --- a/libs/hostgraphics/gui/BufferItem.h +++ b/libs/hostgraphics/include/gui/BufferItem.h diff --git a/libs/hostgraphics/gui/BufferItemConsumer.h b/libs/hostgraphics/include/gui/BufferItemConsumer.h index c25941151800..c25941151800 100644 --- a/libs/hostgraphics/gui/BufferItemConsumer.h +++ b/libs/hostgraphics/include/gui/BufferItemConsumer.h diff --git a/libs/hostgraphics/gui/BufferQueue.h b/libs/hostgraphics/include/gui/BufferQueue.h index 67a8c00fd267..67a8c00fd267 100644 --- a/libs/hostgraphics/gui/BufferQueue.h +++ b/libs/hostgraphics/include/gui/BufferQueue.h diff --git a/libs/hostgraphics/gui/ConsumerBase.h b/libs/hostgraphics/include/gui/ConsumerBase.h index 7f7309e8a3a8..7f7309e8a3a8 100644 --- a/libs/hostgraphics/gui/ConsumerBase.h +++ b/libs/hostgraphics/include/gui/ConsumerBase.h diff --git a/libs/hostgraphics/gui/IGraphicBufferConsumer.h b/libs/hostgraphics/include/gui/IGraphicBufferConsumer.h index 14ac4fe71cc8..14ac4fe71cc8 100644 --- a/libs/hostgraphics/gui/IGraphicBufferConsumer.h +++ b/libs/hostgraphics/include/gui/IGraphicBufferConsumer.h diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/include/gui/IGraphicBufferProducer.h index 8fd8590d10d7..8fd8590d10d7 100644 --- a/libs/hostgraphics/gui/IGraphicBufferProducer.h +++ b/libs/hostgraphics/include/gui/IGraphicBufferProducer.h diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/include/gui/Surface.h index 2774f89cb54c..2774f89cb54c 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/include/gui/Surface.h diff --git a/libs/hostgraphics/ui/Fence.h b/libs/hostgraphics/include/ui/Fence.h index 187c3116f61c..187c3116f61c 100644 --- a/libs/hostgraphics/ui/Fence.h +++ b/libs/hostgraphics/include/ui/Fence.h diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/include/ui/GraphicBuffer.h index cda45e4660ca..cda45e4660ca 100644 --- a/libs/hostgraphics/ui/GraphicBuffer.h +++ b/libs/hostgraphics/include/ui/GraphicBuffer.h diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index c1c30f5379ab..fddcf29b9197 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -25,22 +25,6 @@ namespace android { namespace text_feature { -inline bool fix_double_underline() { -#ifdef __ANDROID__ - return com_android_text_flags_fix_double_underline(); -#else - return true; -#endif // __ANDROID__ -} - -inline bool deprecate_ui_fonts() { -#ifdef __ANDROID__ - return com_android_text_flags_deprecate_ui_fonts(); -#else - return true; -#endif // __ANDROID__ -} - inline bool letter_spacing_justification() { #ifdef __ANDROID__ return com_android_text_flags_letter_spacing_justification(); diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index a2748b050a2d..236c3736816e 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -318,6 +318,11 @@ bool HardwareBitmapUploader::has1010102Support() { return has101012Support; } +bool HardwareBitmapUploader::has10101010Support() { + static bool has1010110Support = checkSupport(AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM); + return has1010110Support; +} + bool HardwareBitmapUploader::hasAlpha8Support() { static bool hasAlpha8Support = checkSupport(AHARDWAREBUFFER_FORMAT_R8_UNORM); return hasAlpha8Support; @@ -376,6 +381,19 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { } formatInfo.format = GL_RGBA; break; + case kRGBA_10x6_SkColorType: + formatInfo.isSupported = HardwareBitmapUploader::has10101010Support(); + if (formatInfo.isSupported) { + formatInfo.type = 0; // Not supported in GL + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM; + formatInfo.vkFormat = VK_FORMAT_R10X6G10X6B10X6A10X6_UNORM_4PACK16; + } else { + formatInfo.type = GL_UNSIGNED_BYTE; + formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + formatInfo.vkFormat = VK_FORMAT_R8G8B8A8_UNORM; + } + formatInfo.format = 0; // Not supported in GL + break; case kAlpha_8_SkColorType: formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); if (formatInfo.isSupported) { diff --git a/libs/hwui/HardwareBitmapUploader.h b/libs/hwui/HardwareBitmapUploader.h index 00ee99648889..76cb80b722d0 100644 --- a/libs/hwui/HardwareBitmapUploader.h +++ b/libs/hwui/HardwareBitmapUploader.h @@ -33,12 +33,14 @@ public: #ifdef __ANDROID__ static bool hasFP16Support(); static bool has1010102Support(); + static bool has10101010Support(); static bool hasAlpha8Support(); #else static bool hasFP16Support() { return true; } static bool has1010102Support() { return true; } + static bool has10101010Support() { return true; } static bool hasAlpha8Support() { return true; } #endif }; diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index 72e83afbd96f..9e825fb350d6 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -841,9 +841,6 @@ void SkiaCanvas::drawGlyphs(ReadGlyphFunc glyphFunc, int count, const Paint& pai sk_sp<SkTextBlob> textBlob(builder.make()); applyLooper(&paintCopy, [&](const SkPaint& p) { mCanvas->drawTextBlob(textBlob, 0, 0, p); }); - if (!text_feature::fix_double_underline()) { - drawTextDecorations(x, y, totalAdvance, paintCopy); - } } void SkiaCanvas::drawLayoutOnPath(const minikin::Layout& layout, float hOffset, float vOffset, diff --git a/libs/hwui/hwui/Canvas.cpp b/libs/hwui/hwui/Canvas.cpp index 80b6c0385fca..5af4af27babd 100644 --- a/libs/hwui/hwui/Canvas.cpp +++ b/libs/hwui/hwui/Canvas.cpp @@ -110,28 +110,26 @@ void Canvas::drawText(const uint16_t* text, int textSize, int start, int count, DrawTextFunctor f(layout, this, paint, x, y, layout.getAdvance()); MinikinUtils::forFontRun(layout, &paint, f); - if (text_feature::fix_double_underline()) { - Paint copied(paint); - PaintFilter* filter = getPaintFilter(); - if (filter != nullptr) { - filter->filterFullPaint(&copied); + Paint copied(paint); + PaintFilter* filter = getPaintFilter(); + if (filter != nullptr) { + filter->filterFullPaint(&copied); + } + const bool isUnderline = copied.isUnderline(); + const bool isStrikeThru = copied.isStrikeThru(); + if (isUnderline || isStrikeThru) { + const SkScalar left = x; + const SkScalar right = x + layout.getAdvance(); + if (isUnderline) { + const SkScalar top = y + f.getUnderlinePosition(); + drawStroke(left, right, top, f.getUnderlineThickness(), copied, this); } - const bool isUnderline = copied.isUnderline(); - const bool isStrikeThru = copied.isStrikeThru(); - if (isUnderline || isStrikeThru) { - const SkScalar left = x; - const SkScalar right = x + layout.getAdvance(); - if (isUnderline) { - const SkScalar top = y + f.getUnderlinePosition(); - drawStroke(left, right, top, f.getUnderlineThickness(), copied, this); - } - if (isStrikeThru) { - float textSize = paint.getSkFont().getSize(); - const float position = textSize * Paint::kStdStrikeThru_Top; - const SkScalar thickness = textSize * Paint::kStdStrikeThru_Thickness; - const SkScalar top = y + position; - drawStroke(left, right, top, thickness, copied, this); - } + if (isStrikeThru) { + float textSize = paint.getSkFont().getSize(); + const float position = textSize * Paint::kStdStrikeThru_Top; + const SkScalar thickness = textSize * Paint::kStdStrikeThru_Thickness; + const SkScalar top = y + position; + drawStroke(left, right, top, thickness, copied, this); } } } diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 0efb2c81af01..d7bf20130b71 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -142,32 +142,30 @@ public: canvas->drawGlyphs(glyphFunc, glyphCount, paint, x, y, totalAdvance); } - if (text_feature::fix_double_underline()) { - // Extract underline position and thickness. - if (paint.isUnderline()) { - SkFontMetrics metrics; - paint.getSkFont().getMetrics(&metrics); - const float textSize = paint.getSkFont().getSize(); - SkScalar position; - if (!metrics.hasUnderlinePosition(&position)) { - position = textSize * Paint::kStdUnderline_Top; - } - SkScalar thickness; - if (!metrics.hasUnderlineThickness(&thickness)) { - thickness = textSize * Paint::kStdUnderline_Thickness; - } - - // If multiple fonts are used, use the most bottom position and most thick stroke - // width as the underline position. This follows the CSS standard: - // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property - // <quote> - // The exact position and thickness of line decorations is UA-defined in this level. - // However, for underlines and overlines the UA must use a single thickness and - // position on each line for the decorations deriving from a single decorating box. - // </quote> - underlinePosition = std::max(underlinePosition, position); - underlineThickness = std::max(underlineThickness, thickness); + // Extract underline position and thickness. + if (paint.isUnderline()) { + SkFontMetrics metrics; + paint.getSkFont().getMetrics(&metrics); + const float textSize = paint.getSkFont().getSize(); + SkScalar position; + if (!metrics.hasUnderlinePosition(&position)) { + position = textSize * Paint::kStdUnderline_Top; } + SkScalar thickness; + if (!metrics.hasUnderlineThickness(&thickness)) { + thickness = textSize * Paint::kStdUnderline_Thickness; + } + + // If multiple fonts are used, use the most bottom position and most thick stroke + // width as the underline position. This follows the CSS standard: + // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property + // <quote> + // The exact position and thickness of line decorations is UA-defined in this level. + // However, for underlines and overlines the UA must use a single thickness and + // position on each line for the decorations deriving from a single decorating box. + // </quote> + underlinePosition = std::max(underlinePosition, position); + underlineThickness = std::max(underlineThickness, thickness); } } diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index d66d7f8e83f4..ede385adc779 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -53,9 +53,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, if (familyVariant.has_value()) { minikinPaint.familyVariant = familyVariant.value(); } else { - minikinPaint.familyVariant = text_feature::deprecate_ui_fonts() - ? minikin::FamilyVariant::ELEGANT - : minikin::FamilyVariant::DEFAULT; + minikinPaint.familyVariant = minikin::FamilyVariant::ELEGANT; } return minikinPaint; } diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index bce84ae77c87..e3023937964e 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -318,6 +318,15 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, tailPNext = &deviceFaultFeatures->pNext; } + if (grExtensions.hasExtension(VK_EXT_RGBA10X6_FORMATS_EXTENSION_NAME, 1)) { + VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT* formatFeatures = + new VkPhysicalDeviceRGBA10X6FormatsFeaturesEXT; + formatFeatures->sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RGBA10X6_FORMATS_FEATURES_EXT; + formatFeatures->pNext = nullptr; + *tailPNext = formatFeatures; + tailPNext = &formatFeatures->pNext; + } + // query to get the physical device features mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features); // this looks like it would slow things down, diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp index c70a30477ecf..ecb06d8ca4db 100644 --- a/libs/hwui/tests/unit/UnderlineTest.cpp +++ b/libs/hwui/tests/unit/UnderlineTest.cpp @@ -109,9 +109,7 @@ DrawTextFunctor processFunctor(const std::vector<uint16_t>& text, Paint* paint) return f; } -TEST_WITH_FLAGS(UnderlineTest, Roboto, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, Roboto) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); @@ -123,9 +121,7 @@ TEST_WITH_FLAGS(UnderlineTest, Roboto, EXPECT_EQ(ROBOTO_THICKNESS_EM * textSize, functor.getUnderlineThickness()); } -TEST_WITH_FLAGS(UnderlineTest, NotoCJK, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, NotoCJK) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); @@ -137,9 +133,7 @@ TEST_WITH_FLAGS(UnderlineTest, NotoCJK, EXPECT_EQ(NOTO_CJK_THICKNESS_EM * textSize, functor.getUnderlineThickness()); } -TEST_WITH_FLAGS(UnderlineTest, Mixture, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::text::flags, - fix_double_underline))) { +TEST(UnderlineTest, Mixture) { float textSize = 100; Paint paint; paint.getSkFont().setSize(textSize); diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 6a560b365247..9673c5f03642 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -49,6 +49,10 @@ static inline SkImageInfo createImageInfo(int32_t width, int32_t height, int32_t colorType = kRGBA_1010102_SkColorType; alphaType = kPremul_SkAlphaType; break; + case AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM: + colorType = kRGBA_10x6_SkColorType; + alphaType = kPremul_SkAlphaType; + break; case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: colorType = kRGBA_F16_SkColorType; alphaType = kPremul_SkAlphaType; @@ -86,6 +90,8 @@ uint32_t ColorTypeToBufferFormat(SkColorType colorType) { return AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM; case kRGBA_1010102_SkColorType: return AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM; + case kRGBA_10x6_SkColorType: + return AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM; case kARGB_4444_SkColorType: // Hardcoding the value from android::PixelFormat static constexpr uint64_t kRGBA4444 = 7; @@ -108,6 +114,8 @@ SkColorType BufferFormatToColorType(uint32_t format) { return kRGB_565_SkColorType; case AHARDWAREBUFFER_FORMAT_R10G10B10A2_UNORM: return kRGBA_1010102_SkColorType; + case AHARDWAREBUFFER_FORMAT_R10G10B10A10_UNORM: + return kRGBA_10x6_SkColorType; case AHARDWAREBUFFER_FORMAT_R16G16B16A16_FLOAT: return kRGBA_F16_SkColorType; case AHARDWAREBUFFER_FORMAT_R8_UNORM: diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index ea5a03613ddc..b28237cb0fbc 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -69,6 +69,29 @@ public final class ApduServiceInfo implements Parcelable { private static final String TAG = "ApduServiceInfo"; /** + * Component level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for a system application to change its icon and label + * on the default applications page. This property should be added to an + * {@link HostApduService} declaration in the manifest. + * + * <p>For example: + * <pre> + * <service + * android:apduServiceBanner="@drawable/product_logo" + * android:label="@string/service_label"> + * <property + * android:name="android.content.PROPERTY_WALLET_ICON_AND_LABEL_HOLDER" + * android:value="true"/> + * </service> + * </pre> + * @hide + */ + @SystemApi + @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ICON_PROPERTY_ENABLED) + public static final String PROPERTY_WALLET_PREFERRED_BANNER_AND_LABEL = + "android.nfc.cardemulation.PROPERTY_WALLET_PREFERRED_BANNER_AND_LABEL"; + + /** * The service that implements this */ private final ResolveInfo mService; diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index ff09084e24cd..c4173ed999f3 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -460,7 +460,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { event.startTracking(); return true; } @@ -479,7 +479,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat return true; } - if (keyCode == KeyEvent.KEYCODE_BACK + if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) && event.isTracking() && !event.isCanceled()) { if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened() && !hasErrors()) { diff --git a/packages/SettingsLib/DataStore/Android.bp b/packages/SettingsLib/DataStore/Android.bp index 86c8f0da83cb..6840e10161f3 100644 --- a/packages/SettingsLib/DataStore/Android.bp +++ b/packages/SettingsLib/DataStore/Android.bp @@ -17,6 +17,7 @@ android_library { "androidx.annotation_annotation", "androidx.collection_collection-ktx", "androidx.core_core-ktx", + "error_prone_annotations", "guava", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt new file mode 100644 index 000000000000..3d4133732915 --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import android.content.SharedPreferences + +/** Interface of key-value store. */ +interface KeyValueStore : KeyedObservable<String> { + + /** Returns if the storage contains persistent value of given key. */ + fun contains(key: String): Boolean + + /** Gets default value of given key. */ + fun <T : Any> getDefaultValue(key: String, valueType: Class<T>): T? = + when (valueType) { + Boolean::class.javaObjectType -> false + Float::class.javaObjectType -> 0f + Int::class.javaObjectType -> 0 + Long::class.javaObjectType -> 0 + else -> null + } + as T? + + /** Gets value of given key. */ + fun <T : Any> getValue(key: String, valueType: Class<T>): T? + + /** + * Sets value for given key. + * + * @param key key + * @param valueType value type + * @param value value to set, null means remove + */ + fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) +} + +/** [SharedPreferences] based [KeyValueStore]. */ +interface SharedPreferencesKeyValueStore : KeyValueStore { + + /** [SharedPreferences] of the key-value store. */ + val sharedPreferences: SharedPreferences + + override fun contains(key: String): Boolean = sharedPreferences.contains(key) + + override fun <T : Any> getValue(key: String, valueType: Class<T>): T? = + when (valueType) { + Boolean::class.javaObjectType -> sharedPreferences.getBoolean(key, false) + Float::class.javaObjectType -> sharedPreferences.getFloat(key, 0f) + Int::class.javaObjectType -> sharedPreferences.getInt(key, 0) + Long::class.javaObjectType -> sharedPreferences.getLong(key, 0) + String::class.javaObjectType -> sharedPreferences.getString(key, null) + Set::class.javaObjectType -> sharedPreferences.getStringSet(key, null) + else -> null + } + as T? + + override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { + if (value == null) { + sharedPreferences.edit().remove(key).apply() + return + } + val edit = sharedPreferences.edit() + when (valueType) { + Boolean::class.javaObjectType -> edit.putBoolean(key, value as Boolean) + Float::class.javaObjectType -> edit.putFloat(key, value as Float) + Int::class.javaObjectType -> edit.putInt(key, value as Int) + Long::class.javaObjectType -> edit.putLong(key, value as Long) + String::class.javaObjectType -> edit.putString(key, value as String) + Set::class.javaObjectType -> edit.putStringSet(key, value as Set<String>) + else -> {} + } + edit.apply() + } +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt index 4ce1d3790e8b..ec903179f496 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt @@ -19,6 +19,7 @@ package com.android.settingslib.datastore import androidx.annotation.AnyThread import androidx.annotation.GuardedBy import androidx.collection.MutableScatterMap +import com.google.errorprone.annotations.CanIgnoreReturnValue import java.util.WeakHashMap import java.util.concurrent.Executor @@ -62,8 +63,9 @@ interface KeyedObservable<K> { * * @param observer observer to be notified * @param executor executor to run the callback + * @return if the observer is newly added */ - fun addObserver(observer: KeyedObserver<K?>, executor: Executor) + @CanIgnoreReturnValue fun addObserver(observer: KeyedObserver<K?>, executor: Executor): Boolean /** * Adds an observer on given key. @@ -73,14 +75,24 @@ interface KeyedObservable<K> { * @param key key to observe * @param observer observer to be notified * @param executor executor to run the callback + * @return if the observer is newly added */ - fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor) + @CanIgnoreReturnValue + fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor): Boolean - /** Removes observer. */ - fun removeObserver(observer: KeyedObserver<K?>) + /** + * Removes observer. + * + * @return if the observer is found and removed + */ + @CanIgnoreReturnValue fun removeObserver(observer: KeyedObserver<K?>): Boolean - /** Removes observer on given key. */ - fun removeObserver(key: K, observer: KeyedObserver<K>) + /** + * Removes observer on given key. + * + * @return if the observer is found and removed + */ + @CanIgnoreReturnValue fun removeObserver(key: K, observer: KeyedObserver<K>): Boolean /** * Notifies all observers that a change occurs. @@ -111,14 +123,17 @@ open class KeyedDataObservable<K> : KeyedObservable<K> { @GuardedBy("itself") private val keyedObservers = MutableScatterMap<K, WeakHashMap<KeyedObserver<K>, Executor>>() - override fun addObserver(observer: KeyedObserver<K?>, executor: Executor) { + @CanIgnoreReturnValue + override fun addObserver(observer: KeyedObserver<K?>, executor: Executor): Boolean { val oldExecutor = synchronized(observers) { observers.put(observer, executor) } if (oldExecutor != null && oldExecutor != executor) { throw IllegalStateException("Add $observer twice, old=$oldExecutor, new=$executor") } + return oldExecutor == null } - override fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor) { + @CanIgnoreReturnValue + override fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor): Boolean { val oldExecutor = synchronized(keyedObservers) { keyedObservers.getOrPut(key) { WeakHashMap() }.put(observer, executor) @@ -126,20 +141,23 @@ open class KeyedDataObservable<K> : KeyedObservable<K> { if (oldExecutor != null && oldExecutor != executor) { throw IllegalStateException("Add $observer twice, old=$oldExecutor, new=$executor") } + return oldExecutor == null } - override fun removeObserver(observer: KeyedObserver<K?>) { - synchronized(observers) { observers.remove(observer) } - } + @CanIgnoreReturnValue + override fun removeObserver(observer: KeyedObserver<K?>) = + synchronized(observers) { observers.remove(observer) } != null - override fun removeObserver(key: K, observer: KeyedObserver<K>) { + @CanIgnoreReturnValue + override fun removeObserver(key: K, observer: KeyedObserver<K>) = synchronized(keyedObservers) { - val observers = keyedObservers[key] - if (observers?.remove(observer) != null && observers.isEmpty()) { + val observers = keyedObservers[key] ?: return false + val removed = observers.remove(observer) != null + if (removed && observers.isEmpty()) { keyedObservers.remove(key) } + removed } - } override fun notifyChange(reason: Int) { // make a copy to avoid potential ConcurrentModificationException diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt new file mode 100644 index 000000000000..4aef0fcfdc15 --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import android.content.ContentResolver +import android.content.Context +import android.provider.Settings.Global +import android.provider.Settings.SettingNotFoundException + +/** + * [KeyValueStore] for [Global] settings. + * + * By default, a boolean type `true` value is stored as `1` and `false` value is stored as `0`. + */ +class SettingsGlobalStore private constructor(contentResolver: ContentResolver) : + SettingsStore(contentResolver) { + + override val tag: String + get() = "SettingsGlobalStore" + + override fun contains(key: String): Boolean = Global.getString(contentResolver, key) != null + + override fun <T : Any> getValue(key: String, valueType: Class<T>): T? = + try { + when (valueType) { + Boolean::class.javaObjectType -> Global.getInt(contentResolver, key) != 0 + Float::class.javaObjectType -> Global.getFloat(contentResolver, key) + Int::class.javaObjectType -> Global.getInt(contentResolver, key) + Long::class.javaObjectType -> Global.getLong(contentResolver, key) + String::class.javaObjectType -> Global.getString(contentResolver, key) + else -> throw UnsupportedOperationException("Get $key $valueType") + } + as T? + } catch (e: SettingNotFoundException) { + null + } + + override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { + if (value == null) { + Global.putString(contentResolver, key, null) + return + } + when (valueType) { + Boolean::class.javaObjectType -> + Global.putInt(contentResolver, key, if (value == true) 1 else 0) + Float::class.javaObjectType -> Global.putFloat(contentResolver, key, value as Float) + Int::class.javaObjectType -> Global.putInt(contentResolver, key, value as Int) + Long::class.javaObjectType -> Global.putLong(contentResolver, key, value as Long) + String::class.javaObjectType -> Global.putString(contentResolver, key, value as String) + else -> throw UnsupportedOperationException("Set $key $valueType") + } + } + + companion object { + @Volatile private var instance: SettingsGlobalStore? = null + + @JvmStatic + fun get(context: Context): SettingsGlobalStore = + instance + ?: synchronized(this) { + instance + ?: SettingsGlobalStore(context.applicationContext.contentResolver).also { + instance = it + } + } + } +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt new file mode 100644 index 000000000000..9f41ecbc7370 --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import android.content.ContentResolver +import android.content.Context +import android.provider.Settings.Secure +import android.provider.Settings.SettingNotFoundException + +/** + * [KeyValueStore] for [Secure] settings. + * + * By default, a boolean type `true` value is stored as `1` and `false` value is stored as `0`. + */ +class SettingsSecureStore private constructor(contentResolver: ContentResolver) : + SettingsStore(contentResolver) { + + override val tag: String + get() = "SettingsSecureStore" + + override fun contains(key: String): Boolean = Secure.getString(contentResolver, key) != null + + override fun <T : Any> getValue(key: String, valueType: Class<T>): T? = + try { + when (valueType) { + Boolean::class.javaObjectType -> Secure.getInt(contentResolver, key) != 0 + Float::class.javaObjectType -> Secure.getFloat(contentResolver, key) + Int::class.javaObjectType -> Secure.getInt(contentResolver, key) + Long::class.javaObjectType -> Secure.getLong(contentResolver, key) + String::class.javaObjectType -> Secure.getString(contentResolver, key) + else -> throw UnsupportedOperationException("Get $key $valueType") + } + as T? + } catch (e: SettingNotFoundException) { + null + } + + override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { + if (value == null) { + Secure.putString(contentResolver, key, null) + return + } + when (valueType) { + Boolean::class.javaObjectType -> + Secure.putInt(contentResolver, key, if (value == true) 1 else 0) + Float::class.javaObjectType -> Secure.putFloat(contentResolver, key, value as Float) + Int::class.javaObjectType -> Secure.putInt(contentResolver, key, value as Int) + Long::class.javaObjectType -> Secure.putLong(contentResolver, key, value as Long) + String::class.javaObjectType -> Secure.putString(contentResolver, key, value as String) + else -> throw UnsupportedOperationException("Set $key $valueType") + } + } + + companion object { + @Volatile private var instance: SettingsSecureStore? = null + + @JvmStatic + fun get(context: Context): SettingsSecureStore = + instance + ?: synchronized(this) { + instance + ?: SettingsSecureStore(context.applicationContext.contentResolver).also { + instance = it + } + } + } +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt new file mode 100644 index 000000000000..59816885f554 --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Log +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicInteger + +/** Base class of the Settings provider data stores. */ +open abstract class SettingsStore(protected val contentResolver: ContentResolver) : + KeyedDataObservable<String>(), KeyValueStore { + + /** + * Counter of observers. + * + * The value is accurate only when [addObserver] and [removeObserver] are called correctly. When + * an observer is not removed (and its weak reference is garbage collected), the content + * observer is not unregistered but this is not a big deal. + */ + private val counter = AtomicInteger() + + private val contentObserver = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + super.onChange(selfChange, null) + } + + override fun onChange(selfChange: Boolean, uri: Uri?) { + val key = uri?.lastPathSegment ?: return + notifyChange(key, DataChangeReason.UPDATE) + } + } + + override fun addObserver(observer: KeyedObserver<String?>, executor: Executor) = + if (super.addObserver(observer, executor)) { + onObserverAdded() + true + } else { + false + } + + override fun addObserver(key: String, observer: KeyedObserver<String>, executor: Executor) = + if (super.addObserver(key, observer, executor)) { + onObserverAdded() + true + } else { + false + } + + private fun onObserverAdded() { + if (counter.getAndIncrement() != 0) return + Log.i(tag, "registerContentObserver") + contentResolver.registerContentObserver( + Settings.Global.getUriFor(""), + true, + contentObserver, + ) + } + + override fun removeObserver(observer: KeyedObserver<String?>) = + if (super.removeObserver(observer)) { + onObserverRemoved() + true + } else { + false + } + + override fun removeObserver(key: String, observer: KeyedObserver<String>) = + if (super.removeObserver(key, observer)) { + onObserverRemoved() + true + } else { + false + } + + private fun onObserverRemoved() { + if (counter.decrementAndGet() != 0) return + Log.i(tag, "unregisterContentObserver") + contentResolver.unregisterContentObserver(contentObserver) + } + + /** Tag for logging. */ + abstract val tag: String +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt new file mode 100644 index 000000000000..6cca7ed59534 --- /dev/null +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import android.content.ContentResolver +import android.content.Context +import android.provider.Settings.SettingNotFoundException +import android.provider.Settings.System + +/** + * [KeyValueStore] for [System] settings. + * + * By default, a boolean type `true` value is stored as `1` and `false` value is stored as `0`. + */ +class SettingsSystemStore private constructor(contentResolver: ContentResolver) : + SettingsStore(contentResolver) { + + override val tag: String + get() = "SettingsSystemStore" + + override fun contains(key: String): Boolean = System.getString(contentResolver, key) != null + + override fun <T : Any> getValue(key: String, valueType: Class<T>): T? = + try { + when (valueType) { + Boolean::class.javaObjectType -> System.getInt(contentResolver, key) != 0 + Float::class.javaObjectType -> System.getFloat(contentResolver, key) + Int::class.javaObjectType -> System.getInt(contentResolver, key) + Long::class.javaObjectType -> System.getLong(contentResolver, key) + String::class.javaObjectType -> System.getString(contentResolver, key) + else -> throw UnsupportedOperationException("Get $key $valueType") + } + as T? + } catch (e: SettingNotFoundException) { + null + } + + override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) { + if (value == null) { + System.putString(contentResolver, key, null) + return + } + when (valueType) { + Boolean::class.javaObjectType -> + System.putInt(contentResolver, key, if (value == true) 1 else 0) + Float::class.javaObjectType -> System.putFloat(contentResolver, key, value as Float) + Int::class.javaObjectType -> System.putInt(contentResolver, key, value as Int) + Long::class.javaObjectType -> System.putLong(contentResolver, key, value as Long) + String::class.javaObjectType -> System.putString(contentResolver, key, value as String) + else -> throw UnsupportedOperationException("Set $key $valueType") + } + } + + companion object { + @Volatile private var instance: SettingsSystemStore? = null + + @JvmStatic + fun get(context: Context): SettingsSystemStore = + instance + ?: synchronized(this) { + instance + ?: SettingsSystemStore(context.applicationContext.contentResolver).also { + instance = it + } + } + } +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt index 0ca91cd4357a..ea17a56715ae 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SharedPreferencesStorage.kt @@ -40,8 +40,9 @@ private fun defaultVerbose() = Build.TYPE == "eng" * Note that existing entries in the SharedPreferences will NOT be deleted before restore. * * @param context Context to get SharedPreferences - * @param name Name of the SharedPreferences - * @param mode Operating mode, see [Context.getSharedPreferences] + * @param name Name of the backup restore storage + * @param sharedPreferences SharedPreferences object + * @param filePath shared preferences file path relative to data dir * @param verbose Verbose logging on key/value pairs during backup/restore. Enable for dev only! * @param filter Filter of key/value pairs for backup and restore. */ @@ -50,12 +51,14 @@ open class SharedPreferencesStorage constructor( context: Context, override val name: String, - @get:VisibleForTesting internal val sharedPreferences: SharedPreferences, + override val sharedPreferences: SharedPreferences, + filePath: String = getSharedPreferencesFilePath(context, name), private val codec: BackupCodec? = null, private val verbose: Boolean = defaultVerbose(), private val filter: (String, Any?) -> Boolean = { _, _ -> true }, ) : - BackupRestoreFileStorage(context, context.getSharedPreferencesFilePath(name)), + BackupRestoreFileStorage(context, filePath), + SharedPreferencesKeyValueStore, KeyedObservable<String> by KeyedDataObservable() { @JvmOverloads @@ -66,7 +69,15 @@ constructor( codec: BackupCodec? = null, verbose: Boolean = defaultVerbose(), filter: (String, Any?) -> Boolean = { _, _ -> true }, - ) : this(context, name, context.getSharedPreferences(name, mode), codec, verbose, filter) + ) : this( + context, + name, + context.getSharedPreferences(name, mode), + getSharedPreferencesFilePath(context, name), + codec, + verbose, + filter, + ) /** Name of the intermediate SharedPreferences. */ @VisibleForTesting @@ -80,7 +91,15 @@ constructor( return context.getSharedPreferences(intermediateName, Context.MODE_MULTI_PROCESS) } - private val sharedPreferencesListener = createSharedPreferenceListener() + private val sharedPreferencesListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key != null) { + notifyChange(key, DataChangeReason.UPDATE) + } else { + // On Android >= R, SharedPreferences.Editor.clear() will trigger this case + notifyChange(DataChangeReason.DELETE) + } + } init { // listener is weakly referenced, so unregister is optional @@ -183,7 +202,8 @@ constructor( else -> { Log.e( LOG_TAG, - "[$name] $operation $key=$value, unknown type: ${value?.javaClass}") + "[$name] $operation $key=$value, unknown type: ${value?.javaClass}", + ) } } } @@ -191,14 +211,31 @@ constructor( } companion object { - private fun Context.getSharedPreferencesFilePath(name: String): String { - val file = getSharedPreferencesFile(name) - return file.relativeTo(dataDirCompat).toString() + /** Returns the storage object of default [SharedPreferences]. */ + @JvmStatic + fun getDefault(context: Context, name: String): SharedPreferencesStorage { + val prefName = getDefaultSharedPreferencesName(context) + return SharedPreferencesStorage( + context, + name, + context.getSharedPreferences(prefName, Context.MODE_PRIVATE), + getSharedPreferencesFilePath(context, prefName), + ) } - /** Returns the absolute path of shared preferences file. */ + /** Returns the name of default [SharedPreferences]. */ @JvmStatic - fun Context.getSharedPreferencesFile(name: String): File { + fun getDefaultSharedPreferencesName(context: Context) = context.packageName + "_preferences" + + /** Returns the shared preferences file path relative to data dir. */ + @JvmStatic + fun getSharedPreferencesFilePath(context: Context, name: String): String { + val file = context.getSharedPreferencesFile(name) + return file.relativeTo(context.dataDirCompat).toString() + } + + /** Returns the absolute path of shared preferences file. */ + private fun Context.getSharedPreferencesFile(name: String): File { // ContextImpl.getSharedPreferencesPath is private return File(getSharedPreferencesDir(), "$name.xml") } diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt index 0fdecb034f83..c99d4b386530 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt @@ -45,14 +45,14 @@ class KeyedObserverTest { @Test fun addObserver_sameExecutor() { - keyedObservable.addObserver(observer1, executor1) - keyedObservable.addObserver(observer1, executor1) + assertThat(keyedObservable.addObserver(observer1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(observer1, executor1)).isFalse() } @Test fun addObserver_keyedObserver_sameExecutor() { - keyedObservable.addObserver(key1, keyedObserver1, executor1) - keyedObservable.addObserver(key1, keyedObserver1, executor1) + assertThat(keyedObservable.addObserver(key1, keyedObserver1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(key1, keyedObserver1, executor1)).isFalse() } @Test @@ -109,15 +109,15 @@ class KeyedObserverTest { @Test fun addObserver_notifyObservers_removeObserver() { - keyedObservable.addObserver(observer1, executor1) - keyedObservable.addObserver(observer2, executor2) + assertThat(keyedObservable.addObserver(observer1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(observer2, executor2)).isTrue() keyedObservable.notifyChange(DataChangeReason.UPDATE) verify(observer1).onKeyChanged(null, DataChangeReason.UPDATE) verify(observer2).onKeyChanged(null, DataChangeReason.UPDATE) reset(observer1, observer2) - keyedObservable.removeObserver(observer2) + assertThat(keyedObservable.removeObserver(observer2)).isTrue() keyedObservable.notifyChange(DataChangeReason.DELETE) verify(observer1).onKeyChanged(null, DataChangeReason.DELETE) @@ -126,15 +126,15 @@ class KeyedObserverTest { @Test fun addObserver_keyedObserver_notifyObservers_removeObserver() { - keyedObservable.addObserver(key1, keyedObserver1, executor1) - keyedObservable.addObserver(key2, keyedObserver2, executor2) + assertThat(keyedObservable.addObserver(key1, keyedObserver1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(key2, keyedObserver2, executor2)).isTrue() keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) verify(keyedObserver1).onKeyChanged(key1, DataChangeReason.UPDATE) verify(keyedObserver2, never()).onKeyChanged(key2, DataChangeReason.UPDATE) reset(keyedObserver1, keyedObserver2) - keyedObservable.removeObserver(key1, keyedObserver1) + assertThat(keyedObservable.removeObserver(key1, keyedObserver1)).isTrue() keyedObservable.notifyChange(key1, DataChangeReason.DELETE) verify(keyedObserver1, never()).onKeyChanged(key1, DataChangeReason.DELETE) @@ -143,9 +143,9 @@ class KeyedObserverTest { @Test fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() { - keyedObservable.addObserver(observer1, executor1) - keyedObservable.addObserver(key1, keyedObserver1, executor1) - keyedObservable.addObserver(key2, keyedObserver2, executor1) + assertThat(keyedObservable.addObserver(observer1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(key1, keyedObserver1, executor1)).isTrue() + assertThat(keyedObservable.addObserver(key2, keyedObserver2, executor1)).isTrue() keyedObservable.notifyChange(DataChangeReason.UPDATE) verify(observer1).onKeyChanged(null, DataChangeReason.UPDATE) @@ -171,25 +171,25 @@ class KeyedObserverTest { fun notifyChange_addObserverWithinCallback() { // ConcurrentModificationException is raised if it is not implemented correctly val observer: KeyedObserver<Any?> = KeyedObserver { _, _ -> - keyedObservable.addObserver(observer1, executor1) + assertThat(keyedObservable.addObserver(observer1, executor1)).isTrue() } - keyedObservable.addObserver(observer, executor1) + assertThat(keyedObservable.addObserver(observer, executor1)).isTrue() keyedObservable.notifyChange(DataChangeReason.UPDATE) - keyedObservable.removeObserver(observer) + assertThat(keyedObservable.removeObserver(observer)).isTrue() } @Test fun notifyChange_KeyedObserver_addObserverWithinCallback() { // ConcurrentModificationException is raised if it is not implemented correctly val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ -> - keyedObservable.addObserver(key1, keyedObserver1, executor1) + assertThat(keyedObservable.addObserver(key1, keyedObserver1, executor1)).isTrue() } - keyedObservable.addObserver(key1, keyObserver, executor1) + assertThat(keyedObservable.addObserver(key1, keyObserver, executor1)).isTrue() keyedObservable.notifyChange(key1, DataChangeReason.UPDATE) - keyedObservable.removeObserver(key1, keyObserver) + assertThat(keyedObservable.removeObserver(key1, keyObserver)).isTrue() } } diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp new file mode 100644 index 000000000000..207637f86372 --- /dev/null +++ b/packages/SettingsLib/Metadata/Android.bp @@ -0,0 +1,23 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "SettingsLibMetadata-srcs", + srcs: ["src/**/*.kt"], +} + +android_library { + name: "SettingsLibMetadata", + defaults: [ + "SettingsLintDefaults", + ], + srcs: [":SettingsLibMetadata-srcs"], + static_libs: [ + "androidx.annotation_annotation", + "androidx.fragment_fragment", + "guava", + "SettingsLibDataStore", + ], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SettingsLib/Metadata/AndroidManifest.xml b/packages/SettingsLib/Metadata/AndroidManifest.xml new file mode 100644 index 000000000000..1c801e640f82 --- /dev/null +++ b/packages/SettingsLib/Metadata/AndroidManifest.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.metadata"> + + <uses-sdk android:minSdkVersion="21" /> +</manifest> diff --git a/packages/SettingsLib/Metadata/processor/Android.bp b/packages/SettingsLib/Metadata/processor/Android.bp new file mode 100644 index 000000000000..d8acc7633d81 --- /dev/null +++ b/packages/SettingsLib/Metadata/processor/Android.bp @@ -0,0 +1,11 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +java_plugin { + name: "SettingsLibMetadata-processor", + srcs: ["src/**/*.kt"], + processor_class: "com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor", + java_resource_dirs: ["resources"], + visibility: ["//visibility:public"], +} diff --git a/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000000..762a01a92f42 --- /dev/null +++ b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor
\ No newline at end of file diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt new file mode 100644 index 000000000000..620d717faf69 --- /dev/null +++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import java.util.TreeMap +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.AnnotationValue +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.type.TypeMirror +import javax.tools.Diagnostic + +/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */ +class PreferenceScreenAnnotationProcessor : AbstractProcessor() { + private val screens = TreeMap<String, ConstructorType>() + private val overlays = mutableMapOf<String, String>() + private val contextType: TypeMirror by lazy { + processingEnv.elementUtils.getTypeElement("android.content.Context").asType() + } + + private var options: Map<String, Any?>? = null + private lateinit var annotationElement: TypeElement + private lateinit var optionsElement: TypeElement + private lateinit var screenType: TypeMirror + + override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS) + + override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported() + + override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + val elementUtils = processingEnv.elementUtils + annotationElement = elementUtils.getTypeElement(ANNOTATION) + optionsElement = elementUtils.getTypeElement(OPTIONS) + screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType() + } + + override fun process( + annotations: MutableSet<out TypeElement>, + roundEnv: RoundEnvironment, + ): Boolean { + roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run { + if (options != null) error("@$OPTIONS_NAME is already specified: $options", this) + options = + annotationMirrors + .single { it.isElement(optionsElement) } + .elementValues + .entries + .associate { it.key.simpleName.toString() to it.value.value } + } + for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) { + (element as? TypeElement)?.process() + } + if (roundEnv.processingOver()) codegen() + return false + } + + private fun TypeElement.process() { + if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) { + error("@$ANNOTATION_NAME must be added to non abstract class", this) + return + } + if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) { + error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this) + return + } + val constructorType = getConstructorType() + if (constructorType == null) { + error( + "Class must be an object, or has single public constructor that " + + "accepts no parameter or a Context parameter", + this, + ) + return + } + val screenQualifiedName = qualifiedName.toString() + screens[screenQualifiedName] = constructorType + val annotation = annotationMirrors.single { it.isElement(annotationElement) } + val overlay = annotation.getOverlay() + if (overlay != null) { + overlays.put(overlay, screenQualifiedName)?.let { + error("$overlay has been overlaid by $it", this) + } + } + } + + private fun codegen() { + val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR + if (collector.isEmpty()) return + val parts = collector.split('/') + if (parts.size == 3) { + generateCode(parts[0], parts[1], parts[2]) + } else { + throw IllegalArgumentException( + "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format" + ) + } + } + + private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) { + for ((overlay, screen) in overlays) { + if (screens.remove(overlay) == null) { + warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME") + } else { + processingEnv.messager.printMessage( + Diagnostic.Kind.NOTE, + "$overlay is overlaid by $screen", + ) + } + } + processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use { + it.write("package $outputPkg;\n\n") + it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n") + it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n") + it.write("public final class $outputClass {\n") + it.write(" private $outputClass() {}\n\n") + it.write( + " public static java.util.List<$PREFERENCE_SCREEN_METADATA> " + + "$outputFun(android.content.Context context) {\n" + ) + it.write( + " java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " + + "new java.util.ArrayList<>(${screens.size});\n" + ) + for ((screen, constructorType) in screens) { + when (constructorType) { + ConstructorType.DEFAULT -> it.write(" screens.add(new $screen());\n") + ConstructorType.CONTEXT -> it.write(" screens.add(new $screen(context));\n") + ConstructorType.SINGLETON -> it.write(" screens.add($screen.INSTANCE);\n") + } + } + for ((overlay, screen) in overlays) { + it.write(" // $overlay is overlaid by $screen\n") + } + it.write(" return screens;\n") + it.write(" }\n") + it.write("}") + } + } + + private fun AnnotationMirror.isElement(element: TypeElement) = + processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType()) + + private fun AnnotationMirror.getOverlay(): String? { + for ((key, value) in elementValues) { + if (key.simpleName.contentEquals("overlay")) { + return if (value.isDefaultClassValue(key)) null else value.value.toString() + } + } + return null + } + + private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) = + processingEnv.typeUtils.isSameType( + value as TypeMirror, + key.defaultValue.value as TypeMirror, + ) + + private fun TypeElement.getConstructorType(): ConstructorType? { + var constructor: ExecutableElement? = null + for (element in enclosedElements) { + if (element.isKotlinObject()) return ConstructorType.SINGLETON + if (element.kind != ElementKind.CONSTRUCTOR) continue + if (!element.modifiers.contains(Modifier.PUBLIC)) continue + if (constructor != null) return null + constructor = element as ExecutableElement + } + return constructor?.parameters?.run { + when { + isEmpty() -> ConstructorType.DEFAULT + size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) -> + ConstructorType.CONTEXT + else -> null + } + } + } + + private fun Element.isKotlinObject() = + kind == ElementKind.FIELD && + modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } && + simpleName.toString() == "INSTANCE" + + private fun warn(msg: CharSequence) = + processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg) + + private fun error(msg: CharSequence, element: Element) = + processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element) + + private enum class ConstructorType { + DEFAULT, // default constructor with no parameter + CONTEXT, // constructor with a Context parameter + SINGLETON, // Kotlin object class + } + + companion object { + private const val PACKAGE = "com.android.settingslib.metadata" + private const val ANNOTATION_NAME = "ProvidePreferenceScreen" + private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME" + private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata" + + private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions" + private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME" + private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get" + } +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt new file mode 100644 index 000000000000..ea20a74de3cf --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import kotlin.reflect.KClass + +/** + * Annotation to provide preference screen. + * + * The annotated class must satisfy either condition: + * - the primary constructor has no parameter + * - the primary constructor has a single [android.content.Context] parameter + * - it is a Kotlin object class + * + * @param overlay if specified, current annotated screen will overlay the given screen + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class ProvidePreferenceScreen( + val overlay: KClass<out PreferenceScreenMetadata> = PreferenceScreenMetadata::class, +) + +/** + * Provides options for [ProvidePreferenceScreen] annotation processor. + * + * @param codegenCollector generated collector class (format: "pkg/class/method"), an empty string + * means do not generate code + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class ProvidePreferenceScreenOptions( + val codegenCollector: String = "com.android.settingslib.metadata/PreferenceScreenCollector/get", +) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt new file mode 100644 index 000000000000..51a85803c6ed --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context +import androidx.annotation.ArrayRes +import androidx.annotation.IntDef +import com.android.settingslib.datastore.KeyValueStore + +/** Permit of read and write request. */ +@IntDef( + ReadWritePermit.ALLOW, + ReadWritePermit.DISALLOW, + ReadWritePermit.REQUIRE_APP_PERMISSION, + ReadWritePermit.REQUIRE_USER_AGREEMENT, +) +@Retention(AnnotationRetention.SOURCE) +annotation class ReadWritePermit { + companion object { + /** Allow to read/write value. */ + const val ALLOW = 0 + /** Disallow to read/write value (e.g. uid not allowed). */ + const val DISALLOW = 1 + /** Require (runtime/special) app permission from user explicitly. */ + const val REQUIRE_APP_PERMISSION = 2 + /** Require explicit user agreement (e.g. terms of service). */ + const val REQUIRE_USER_AGREEMENT = 3 + } +} + +/** Preference interface that has a value persisted in datastore. */ +interface PersistentPreference<T> { + + /** + * Returns the key-value storage of the preference. + * + * The default implementation returns the storage provided by + * [PreferenceScreenRegistry.getKeyValueStore]. + */ + fun storage(context: Context): KeyValueStore = + PreferenceScreenRegistry.getKeyValueStore(context, this as PreferenceMetadata)!! + + /** + * Returns if the external application (identified by [callingUid]) has permission to read + * preference value. + * + * The underlying implementation does NOT need to check common states like isEnabled, + * isRestricted or isAvailable. + */ + @ReadWritePermit + fun getReadPermit(context: Context, myUid: Int, callingUid: Int): Int = + PreferenceScreenRegistry.getReadPermit( + context, + myUid, + callingUid, + this as PreferenceMetadata, + ) + + /** + * Returns if the external application (identified by [callingUid]) has permission to write + * preference with given [value]. + * + * The underlying implementation does NOT need to check common states like isEnabled, + * isRestricted or isAvailable. + */ + @ReadWritePermit + fun getWritePermit(context: Context, value: T?, myUid: Int, callingUid: Int): Int = + PreferenceScreenRegistry.getWritePermit( + context, + value, + myUid, + callingUid, + this as PreferenceMetadata, + ) +} + +/** Descriptor of values. */ +sealed interface ValueDescriptor { + + /** Returns if given value (represented by index) is valid. */ + fun isValidValue(context: Context, index: Int): Boolean +} + +/** + * A boolean type value. + * + * A zero value means `False`, otherwise it is `True`. + */ +interface BooleanValue : ValueDescriptor { + override fun isValidValue(context: Context, index: Int) = true +} + +/** Value falls into a given array. */ +interface DiscreteValue<T> : ValueDescriptor { + @get:ArrayRes val values: Int + + @get:ArrayRes val valuesDescription: Int + + fun getValue(context: Context, index: Int): T +} + +/** + * Value falls into a text array, whose element is [CharSequence] type. + * + * [values] resource is `<string-array>`. + */ +interface DiscreteTextValue : DiscreteValue<CharSequence> { + override fun isValidValue(context: Context, index: Int): Boolean { + if (index < 0) return false + return index < context.resources.getTextArray(values).size + } + + override fun getValue(context: Context, index: Int): CharSequence = + context.resources.getTextArray(values)[index] +} + +/** + * Value falls into a string array, whose element is [String] type. + * + * [values] resource is `<string-array>`. + */ +interface DiscreteStringValue : DiscreteValue<String> { + override fun isValidValue(context: Context, index: Int): Boolean { + if (index < 0) return false + return index < context.resources.getStringArray(values).size + } + + override fun getValue(context: Context, index: Int): String = + context.resources.getStringArray(values)[index] +} + +/** + * Value falls into an integer array. + * + * [values] resource is `<integer-array>`. + */ +interface DiscreteIntValue : DiscreteValue<Int> { + override fun isValidValue(context: Context, index: Int): Boolean { + if (index < 0) return false + return index < context.resources.getIntArray(values).size + } + + override fun getValue(context: Context, index: Int): Int = + context.resources.getIntArray(values)[index] +} + +/** Value is between a range. */ +interface RangeValue : ValueDescriptor { + /** The lower bound (inclusive) of the range. */ + val minValue: Int + + /** The upper bound (inclusive) of the range. */ + val maxValue: Int + + /** The increment step within the range. 0 means unset, which implies step size is 1. */ + val incrementStep: Int + get() = 0 + + override fun isValidValue(context: Context, index: Int) = index in minValue..maxValue +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt new file mode 100644 index 000000000000..450373804b28 --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */ +open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) + +/** + * Preference hierarchy describes the structure of preferences recursively. + * + * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group. + */ +class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) : + PreferenceHierarchyNode(metadata) { + + private val children = mutableListOf<PreferenceHierarchyNode>() + + /** Adds a preference to the hierarchy. */ + operator fun PreferenceMetadata.unaryPlus() { + children.add(PreferenceHierarchyNode(this)) + } + + /** + * Adds preference screen with given key (as a placeholder) to the hierarchy. + * + * This is mainly to support Android Settings overlays. OEMs might want to custom some of the + * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or + * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects. + * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will + * be looked up from [PreferenceScreenRegistry] lazily at runtime. + * + * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] + */ + operator fun String.unaryPlus() { + children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!)) + } + + /** Adds a preference to the hierarchy. */ + fun add(metadata: PreferenceMetadata) { + children.add(PreferenceHierarchyNode(metadata)) + } + + /** Adds a preference group to the hierarchy. */ + operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) } + + /** Adds a preference group and returns its preference hierarchy. */ + fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy = + PreferenceHierarchy(metadata).also { children.add(it) } + + /** + * Adds preference screen with given key (as a placeholder) to the hierarchy. + * + * This is mainly to support Android Settings overlays. OEMs might want to custom some of the + * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or + * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects. + * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will + * be looked up from [PreferenceScreenRegistry] lazily at runtime. + * + * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry] + */ + fun addPreferenceScreen(screenKey: String) { + children.add(PreferenceHierarchy(PreferenceScreenRegistry[screenKey]!!)) + } + + /** Extensions to add more preferences to the hierarchy. */ + operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this) + + /** Traversals preference hierarchy and applies given action. */ + fun forEach(action: (PreferenceHierarchyNode) -> Unit) { + for (it in children) action(it) + } + + /** Traversals preference hierarchy and applies given action. */ + suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) { + for (it in children) action(it) + } + + /** Finds the [PreferenceMetadata] object associated with given key. */ + fun find(key: String): PreferenceMetadata? { + if (metadata.key == key) return metadata + for (child in children) { + if (child is PreferenceHierarchy) { + val result = child.find(key) + if (result != null) return result + } else { + if (child.metadata.key == key) return child.metadata + } + } + return null + } + + /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */ + fun getAllPreferences(): List<PreferenceMetadata> = + mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) } + + private fun getAllPreferences(result: MutableList<PreferenceMetadata>) { + result.add(metadata) + for (child in children) { + if (child is PreferenceHierarchy) { + child.getAllPreferences(result) + } else { + result.add(child.metadata) + } + } + } +} + +/** + * Builder function to create [PreferenceHierarchy] in + * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner. + */ +fun preferenceHierarchy(metadata: PreferenceMetadata, init: PreferenceHierarchy.() -> Unit) = + PreferenceHierarchy(metadata).also(init) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt new file mode 100644 index 000000000000..f39f3a065e79 --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.annotation.AnyThread +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +/** + * Interface provides preference metadata (title, summary, icon, etc.). + * + * Besides the existing APIs, subclass could integrate with following interface to provide more + * information: + * - [PreferenceTitleProvider]: provide dynamic title content + * - [PreferenceSummaryProvider]: provide dynamic summary content (e.g. based on preference value) + * - [PreferenceAvailabilityProvider]: provide preference availability (e.g. based on flag) + * - [PreferenceLifecycleProvider]: provide the lifecycle callbacks and notify state change + * + * Notes: + * - UI framework support: + * - This class does not involve any UI logic, it is the data layer. + * - Subclass could integrate with datastore and UI widget to provide UI layer. For instance, + * `PreferenceBinding` supports Jetpack Preference binding. + * - Datastore: + * - Subclass should implement the [PersistentPreference] to note that current preference is + * persistent in datastore. + * - It is always recommended to support back up preference value changed by user. Typically, + * the back up and restore happen within datastore, the [allowBackup] API is to mark if + * current preference value should be backed up (backup allowed by default). + * - Preference indexing for search: + * - Override [isIndexable] API to mark if preference is indexable (enabled by default). + * - If [isIndexable] returns true, preference title and summary will be indexed with cache. + * More indexing data could be provided through [keywords]. + * - Settings search will cache the preference title/summary/keywords for indexing. The cache is + * invalidated when system locale changed, app upgraded, etc. + * - Dynamic content is not suitable to be cached for indexing. Subclass that implements + * [PreferenceTitleProvider] / [PreferenceSummaryProvider] will not have its title / summary + * indexed. + */ +@AnyThread +interface PreferenceMetadata { + + /** Preference key. */ + val key: String + + /** + * Preference title resource id. + * + * Implement [PreferenceTitleProvider] if title is generated dynamically. + */ + val title: Int + @StringRes get() = 0 + + /** + * Preference summary resource id. + * + * Implement [PreferenceSummaryProvider] if summary is generated dynamically (e.g. summary is + * provided per preference value) + */ + val summary: Int + @StringRes get() = 0 + + /** Icon of the preference. */ + val icon: Int + @DrawableRes get() = 0 + + /** Additional keywords for indexing. */ + val keywords: Int + @StringRes get() = 0 + + /** + * Return the extras Bundle object associated with this preference. + * + * It is used to provide more information for metadata. + */ + fun extras(context: Context): Bundle? = null + + /** + * Returns if preference is indexable, default value is `true`. + * + * Return `false` only when the preference is always unavailable on current device. If it is + * conditional available, override [PreferenceAvailabilityProvider]. + */ + fun isIndexable(context: Context): Boolean = true + + /** + * Returns if preference is enabled. + * + * UI framework normally does not allow user to interact with the preference widget when it is + * disabled. + * + * [dependencyOfEnabledState] is provided to support dependency, the [shouldDisableDependents] + * value of dependent preference is used to decide enabled state. + */ + fun isEnabled(context: Context): Boolean { + val dependency = dependencyOfEnabledState(context) ?: return true + return !dependency.shouldDisableDependents(context) + } + + /** Returns the key of depended preference to decide the enabled state. */ + fun dependencyOfEnabledState(context: Context): PreferenceMetadata? = null + + /** Returns whether this preference's dependents should be disabled. */ + fun shouldDisableDependents(context: Context): Boolean = !isEnabled(context) + + /** Returns if the preference is persistent in datastore. */ + fun isPersistent(context: Context): Boolean = this is PersistentPreference<*> + + /** + * Returns if preference value backup is allowed (by default returns `true` if preference is + * persistent). + */ + fun allowBackup(context: Context): Boolean = isPersistent(context) + + /** Returns preference intent. */ + fun intent(context: Context): Intent? = null + + /** Returns preference order. */ + fun order(context: Context): Int? = null + + /** + * Returns the preference title. + * + * Implement [PreferenceTitleProvider] interface if title content is generated dynamically. + */ + fun getPreferenceTitle(context: Context): CharSequence? = + when { + title != 0 -> context.getText(title) + this is PreferenceTitleProvider -> getTitle(context) + else -> null + } + + /** + * Returns the preference summary. + * + * Implement [PreferenceSummaryProvider] interface if summary content is generated dynamically + * (e.g. summary is provided per preference value). + */ + fun getPreferenceSummary(context: Context): CharSequence? = + when { + summary != 0 -> context.getText(summary) + this is PreferenceSummaryProvider -> getSummary(context) + else -> null + } +} + +/** Metadata of preference group. */ +@AnyThread +open class PreferenceGroup(override val key: String, override val title: Int) : PreferenceMetadata + +/** Metadata of preference screen. */ +@AnyThread +interface PreferenceScreenMetadata : PreferenceMetadata { + + /** + * The screen title resource, which precedes [getScreenTitle] if provided. + * + * By default, screen title is same with [title]. + */ + val screenTitle: Int + get() = title + + /** Returns dynamic screen title, use [screenTitle] whenever possible. */ + fun getScreenTitle(context: Context): CharSequence? = null + + /** Returns the fragment class to show the preference screen. */ + fun fragmentClass(): Class<out Fragment>? + + /** + * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen. + * + * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen. + * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is + * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in + * [getPreferenceHierarchy] will only be used to bind UI widgets. + */ + fun hasCompleteHierarchy(): Boolean = true + + /** + * Returns the hierarchy of preference screen. + * + * The implementation MUST include all preferences into the hierarchy regardless of the runtime + * conditions. DO NOT check any condition (except compile time flag) before adding a preference. + */ + fun getPreferenceHierarchy(context: Context): PreferenceHierarchy +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt new file mode 100644 index 000000000000..84014f191f68 --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context + +/** Provides the associated preference screen key for binding. */ +interface PreferenceScreenBindingKeyProvider { + + /** Returns the associated preference screen key. */ + fun getPreferenceScreenBindingKey(context: Context): String? +} + +/** Extra key to provide the preference screen key for binding. */ +const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key" diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt new file mode 100644 index 000000000000..48798da57dae --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context +import com.android.settingslib.datastore.KeyValueStore +import com.google.common.base.Supplier +import com.google.common.base.Suppliers +import com.google.common.collect.ImmutableMap + +private typealias PreferenceScreenMap = ImmutableMap<String, PreferenceScreenMetadata> + +/** Registry of all available preference screens in the app. */ +object PreferenceScreenRegistry : ReadWritePermitProvider { + + /** Provider of key-value store. */ + private lateinit var keyValueStoreProvider: KeyValueStoreProvider + + private var preferenceScreensSupplier: Supplier<PreferenceScreenMap> = Supplier { + ImmutableMap.of() + } + + private val preferenceScreens: PreferenceScreenMap + get() = preferenceScreensSupplier.get() + + private var readWritePermitProvider: ReadWritePermitProvider? = null + + /** Sets the [KeyValueStoreProvider]. */ + fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) { + this.keyValueStoreProvider = keyValueStoreProvider + } + + /** + * Returns the key-value store for given preference. + * + * Must call [setKeyValueStoreProvider] before invoking this method, otherwise + * [NullPointerException] is raised. + */ + fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? = + keyValueStoreProvider.getKeyValueStore(context, preference) + + /** Sets supplier to provide available preference screens. */ + fun setPreferenceScreensSupplier(supplier: Supplier<List<PreferenceScreenMetadata>>) { + preferenceScreensSupplier = + Suppliers.memoize { + val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>() + for (screen in supplier.get()) screensBuilder.put(screen.key, screen) + screensBuilder.buildOrThrow() + } + } + + /** Sets available preference screens. */ + fun setPreferenceScreens(vararg screens: PreferenceScreenMetadata) { + val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>() + for (screen in screens) screensBuilder.put(screen.key, screen) + preferenceScreensSupplier = Suppliers.ofInstance(screensBuilder.buildOrThrow()) + } + + /** Returns [PreferenceScreenMetadata] of particular key. */ + operator fun get(key: String?): PreferenceScreenMetadata? = + if (key != null) preferenceScreens[key] else null + + /** + * Sets the provider to check read write permit. Read and write requests are denied by default. + */ + fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider?) { + this.readWritePermitProvider = readWritePermitProvider + } + + override fun getReadPermit( + context: Context, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ) = + readWritePermitProvider?.getReadPermit(context, myUid, callingUid, preference) + ?: ReadWritePermit.DISALLOW + + override fun getWritePermit( + context: Context, + value: Any?, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ) = + readWritePermitProvider?.getWritePermit(context, value, myUid, callingUid, preference) + ?: ReadWritePermit.DISALLOW +} + +/** Provider of [KeyValueStore]. */ +fun interface KeyValueStoreProvider { + + /** + * Returns the key-value store for given preference. + * + * Here are some use cases: + * - provide the default storage for all preferences + * - determine the storage per preference keys or the interfaces implemented by the preference + */ + fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? +} + +/** Provider of read and write permit. */ +interface ReadWritePermitProvider { + + @ReadWritePermit + fun getReadPermit( + context: Context, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ): Int + + @ReadWritePermit + fun getWritePermit( + context: Context, + value: Any?, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ): Int + + companion object { + @JvmField + val ALLOW_ALL_READ_WRITE = + object : ReadWritePermitProvider { + override fun getReadPermit( + context: Context, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ) = ReadWritePermit.ALLOW + + override fun getWritePermit( + context: Context, + value: Any?, + myUid: Int, + callingUid: Int, + preference: PreferenceMetadata, + ) = ReadWritePermit.ALLOW + } + } +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt new file mode 100644 index 000000000000..a3aa85df5325 --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context + +/** + * Interface to provide dynamic preference title. + * + * Implement this interface implies that the preference title should not be cached for indexing. + */ +interface PreferenceTitleProvider { + + /** Provides preference title. */ + fun getTitle(context: Context): CharSequence? +} + +/** + * Interface to provide dynamic preference summary. + * + * Implement this interface implies that the preference summary should not be cached for indexing. + */ +interface PreferenceSummaryProvider { + + /** Provides preference summary. */ + fun getSummary(context: Context): CharSequence? +} + +/** + * Interface to provide the state of preference availability. + * + * UI framework normally does not show the preference widget if it is unavailable. + */ +interface PreferenceAvailabilityProvider { + + /** Returns if the preference is available. */ + fun isAvailable(context: Context): Boolean +} + +/** + * Interface to provide the managed configuration state of the preference. + * + * See [Managed configurations](https://developer.android.com/work/managed-configurations) for the + * Android Enterprise support. + */ +interface PreferenceRestrictionProvider { + + /** Returns if preference is restricted by managed configs. */ + fun isRestricted(context: Context): Boolean +} + +/** + * Preference lifecycle to deal with preference state. + * + * Implement this interface when preference depends on runtime conditions. + */ +interface PreferenceLifecycleProvider { + + /** + * Called when preference is attached to UI. + * + * Subclass could override this API to register runtime condition listeners, and invoke + * `onPreferenceStateChanged(this)` on the given [preferenceStateObserver] to update UI when + * internal state (e.g. availability, enabled state, title, summary) is changed. + */ + fun onAttach(context: Context, preferenceStateObserver: PreferenceStateObserver) + + /** + * Called when preference is detached from UI. + * + * Clean up and release resource. + */ + fun onDetach(context: Context) + + /** Observer of preference state. */ + interface PreferenceStateObserver { + + /** Callbacks when preference state is changed. */ + fun onPreferenceStateChanged(preference: PreferenceMetadata) + } +} diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt new file mode 100644 index 000000000000..ad996c7c8f86 --- /dev/null +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.metadata + +import android.content.Context +import androidx.annotation.StringRes + +/** + * Common base class for preferences that have two selectable states, save a boolean value, and may + * have dependent preferences that are enabled/disabled based on the current state. + */ +interface TwoStatePreference : PreferenceMetadata, PersistentPreference<Boolean>, BooleanValue { + + override fun shouldDisableDependents(context: Context) = + storage(context).getValue(key, Boolean::class.javaObjectType) != true || + super.shouldDisableDependents(context) +} + +/** A preference that provides a two-state toggleable option. */ +open class SwitchPreference +@JvmOverloads +constructor( + override val key: String, + @StringRes override val title: Int = 0, + @StringRes override val summary: Int = 0, +) : TwoStatePreference diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp new file mode 100644 index 000000000000..9665dbd17e2d --- /dev/null +++ b/packages/SettingsLib/Preference/Android.bp @@ -0,0 +1,23 @@ +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +filegroup { + name: "SettingsLibPreference-srcs", + srcs: ["src/**/*.kt"], +} + +android_library { + name: "SettingsLibPreference", + defaults: [ + "SettingsLintDefaults", + ], + srcs: [":SettingsLibPreference-srcs"], + static_libs: [ + "SettingsLibDataStore", + "SettingsLibMetadata", + "androidx.annotation_annotation", + "androidx.preference_preference", + ], + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml new file mode 100644 index 000000000000..2d7f7ba5ec40 --- /dev/null +++ b/packages/SettingsLib/Preference/AndroidManifest.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.preference"> + + <uses-sdk android:minSdkVersion="21" /> +</manifest> diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt new file mode 100644 index 000000000000..9be0e7194859 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import androidx.preference.DialogPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import androidx.preference.SeekBarPreference +import com.android.settingslib.metadata.DiscreteIntValue +import com.android.settingslib.metadata.DiscreteValue +import com.android.settingslib.metadata.PreferenceAvailabilityProvider +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.PreferenceScreenMetadata +import com.android.settingslib.metadata.RangeValue + +/** Binding of preference widget and preference metadata. */ +interface PreferenceBinding { + + /** + * Provides a new [Preference] widget instance. + * + * By default, it returns a new [Preference] object. Subclass could override this method to + * provide customized widget and do **one-off** initialization (e.g. + * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed, + * override the [bind] method. + * + * Notes: + * - DO NOT set any properties defined in [PreferenceMetadata]. For example, + * title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties + * will be reset by [bind]. + * - Override [bind] if needed to provide more information for customized widget. + */ + fun createWidget(context: Context): Preference = Preference(context) + + /** + * Binds preference widget with given metadata. + * + * Whenever metadata state is changed, this callback is invoked to update widget. By default, + * the common states like title, summary, enabled, etc. are already applied. Subclass should + * override this method to bind more data (e.g. read preference value from storage and apply it + * to widget). + * + * @param preference preference widget created by [createWidget] + * @param metadata metadata to apply + */ + fun bind(preference: Preference, metadata: PreferenceMetadata) { + metadata.apply { + preference.key = key + if (icon != 0) { + preference.setIcon(icon) + } else { + preference.icon = null + } + val context = preference.context + preference.peekExtras()?.clear() + extras(context)?.let { preference.extras.putAll(it) } + preference.title = getPreferenceTitle(context) + preference.summary = getPreferenceSummary(context) + preference.isEnabled = isEnabled(context) + preference.isVisible = + (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false + preference.isPersistent = isPersistent(context) + metadata.order(context)?.let { preference.order = it } + // PreferenceRegistry will notify dependency change, so we do not need to set + // dependency here. This simplifies dependency management and avoid the + // IllegalStateException when call Preference.setDependency + preference.dependency = null + if (preference !is PreferenceScreen) { // avoid recursive loop when build graph + preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name + preference.intent = intent(context) + } + if (preference is DialogPreference) { + preference.dialogTitle = preference.title + } + if (preference is ListPreference && this is DiscreteValue<*>) { + preference.setEntries(valuesDescription) + if (this is DiscreteIntValue) { + val intValues = context.resources.getIntArray(values) + preference.entryValues = Array(intValues.size) { intValues[it].toString() } + } else { + preference.setEntryValues(values) + } + } else if (preference is SeekBarPreference && this is RangeValue) { + preference.min = minValue + preference.max = maxValue + preference.seekBarIncrement = incrementStep + } + } + } +} + +/** Abstract preference screen to provide preference hierarchy and binding factory. */ +interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider { + + val preferenceBindingFactory: PreferenceBindingFactory + get() = DefaultPreferenceBindingFactory + + override fun createPreferenceScreen(factory: PreferenceScreenFactory) = + factory.getOrCreatePreferenceScreen().apply { + inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context)) + } +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt new file mode 100644 index 000000000000..4c2e1ba683f6 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import com.android.settingslib.metadata.PreferenceGroup +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.SwitchPreference + +/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */ +interface PreferenceBindingFactory { + + /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */ + fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding? +} + +/** Default [PreferenceBindingFactory]. */ +object DefaultPreferenceBindingFactory : PreferenceBindingFactory { + + override fun getPreferenceBinding(metadata: PreferenceMetadata) = + metadata as? PreferenceBinding + ?: when (metadata) { + is SwitchPreference -> SwitchPreferenceBinding.INSTANCE + is PreferenceGroup -> PreferenceGroupBinding.INSTANCE + is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE + else -> DefaultPreferenceBinding + } +} + +/** A preference key based binding factory. */ +class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) : + PreferenceBindingFactory { + + override fun getPreferenceBinding(metadata: PreferenceMetadata) = + bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata) +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt new file mode 100644 index 000000000000..ede970e42e72 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY +import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.PreferenceScreenMetadata +import com.android.settingslib.metadata.PreferenceTitleProvider + +/** Binding of preference group associated with [PreferenceCategory]. */ +interface PreferenceScreenBinding : PreferenceBinding { + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + val context = preference.context + val screenMetadata = metadata as PreferenceScreenMetadata + // Pass the preference key to fragment, so that the fragment could find associated + // preference screen registered in PreferenceScreenRegistry + preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key) + if (preference is PreferenceScreen) { + val screenTitle = screenMetadata.screenTitle + preference.title = + if (screenTitle != 0) { + context.getString(screenTitle) + } else { + screenMetadata.getScreenTitle(context) + ?: (this as? PreferenceTitleProvider)?.getTitle(context) + } + } + } + + companion object { + @JvmStatic val INSTANCE = object : PreferenceScreenBinding {} + } +} + +/** Binding of preference group associated with [PreferenceCategory]. */ +interface PreferenceGroupBinding : PreferenceBinding { + + override fun createWidget(context: Context) = PreferenceCategory(context) + + companion object { + @JvmStatic val INSTANCE = object : PreferenceGroupBinding {} + } +} + +/** A boolean value type preference associated with [SwitchPreferenceCompat]. */ +interface SwitchPreferenceBinding : PreferenceBinding { + + override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context) + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + (metadata as? PersistentPreference<*>) + ?.storage(preference.context) + ?.getValue(metadata.key, Boolean::class.javaObjectType) + ?.let { (preference as SwitchPreferenceCompat).isChecked = it } + } + + companion object { + @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {} + } +} + +/** Default [PreferenceBinding] for [Preference]. */ +object DefaultPreferenceBinding : PreferenceBinding diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt new file mode 100644 index 000000000000..02acfca6f149 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import androidx.preference.PreferenceDataStore +import com.android.settingslib.datastore.KeyValueStore + +/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */ +class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() { + + override fun getBoolean(key: String, defValue: Boolean): Boolean = + keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue + + override fun getFloat(key: String, defValue: Float): Float = + keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue + + override fun getInt(key: String, defValue: Int): Int = + keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue + + override fun getLong(key: String, defValue: Long): Long = + keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue + + override fun getString(key: String, defValue: String?): String? = + keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue + + override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? = + (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues + + override fun putBoolean(key: String, value: Boolean) = + keyValueStore.setValue(key, Boolean::class.javaObjectType, value) + + override fun putFloat(key: String, value: Float) = + keyValueStore.setValue(key, Float::class.javaObjectType, value) + + override fun putInt(key: String, value: Int) = + keyValueStore.setValue(key, Int::class.javaObjectType, value) + + override fun putLong(key: String, value: Long) = + keyValueStore.setValue(key, Long::class.javaObjectType, value) + + override fun putString(key: String, value: String?) = + keyValueStore.setValue(key, String::class.javaObjectType, value) + + override fun putStringSet(key: String, values: Set<String>?) = + keyValueStore.setValue(key, Set::class.javaObjectType, values) +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt new file mode 100644 index 000000000000..207200998b05 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import android.os.Bundle +import androidx.annotation.XmlRes +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY +import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider +import com.android.settingslib.metadata.PreferenceScreenRegistry +import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively + +/** Fragment to display a preference screen. */ +open class PreferenceFragment : + PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider { + + private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceScreen = createPreferenceScreen() + } + + fun createPreferenceScreen(): PreferenceScreen? = + createPreferenceScreen(PreferenceScreenFactory(this)) + + override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? { + val context = factory.context + fun createPreferenceScreenFromResource() = + factory.inflate(getPreferenceScreenResId(context)) + + if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource() + + val screenKey = getPreferenceScreenBindingKey(context) + val screenCreator = + (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator) + ?: return createPreferenceScreenFromResource() + + val preferenceBindingFactory = screenCreator.preferenceBindingFactory + val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context) + val preferenceScreen = + if (screenCreator.hasCompleteHierarchy()) { + factory.getOrCreatePreferenceScreen().apply { + inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy) + } + } else { + createPreferenceScreenFromResource()?.also { + bindRecursively(it, preferenceBindingFactory, preferenceHierarchy) + } ?: return null + } + preferenceScreenBindingHelper = + PreferenceScreenBindingHelper( + context, + preferenceBindingFactory, + preferenceScreen, + preferenceHierarchy, + ) + return preferenceScreen + } + + /** + * Returns if preference screen metadata can be used to set up preference screen. + * + * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build + * preference screen. + */ + protected open fun usePreferenceScreenMetadata(): Boolean = true + + /** Returns the xml resource to create preference screen. */ + @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0 + + override fun getPreferenceScreenBindingKey(context: Context): String? = + arguments?.getString(EXTRA_BINDING_SCREEN_KEY) + + override fun onDestroy() { + preferenceScreenBindingHelper?.close() + super.onDestroy() + } + + companion object { + /** Returns [PreferenceFragment] instance to display the preference screen of given key. */ + fun of(screenKey: String): PreferenceFragment? { + val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null + if ( + screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy() + ) { + return PreferenceFragment().apply { + arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) } + } + } + return null + } + } +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt new file mode 100644 index 000000000000..5ef7823a4745 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import androidx.preference.PreferenceDataStore +import androidx.preference.PreferenceGroup +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceHierarchy +import com.android.settingslib.metadata.PreferenceMetadata + +/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */ +fun PreferenceGroup.inflatePreferenceHierarchy( + preferenceBindingFactory: PreferenceBindingFactory, + hierarchy: PreferenceHierarchy, + storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(), +) { + fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this) + + hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) } + hierarchy.forEach { + val metadata = it.metadata + val preferenceBinding = metadata.preferenceBinding() ?: return@forEach + val widget = preferenceBinding.createWidget(context) + if (it is PreferenceHierarchy) { + val preferenceGroup = widget as PreferenceGroup + // MUST add preference before binding, otherwise exception is raised when add child + addPreference(preferenceGroup) + preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it) + } else { + preferenceBinding.bind(widget, metadata) + (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage -> + widget.preferenceDataStore = + storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) } + } + // MUST add preference after binding for persistent preference to get initial value + // (preference key is set within bind method) + addPreference(widget) + } + } +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt new file mode 100644 index 000000000000..3610894c3fc0 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.preference.Preference +import androidx.preference.PreferenceGroup +import androidx.preference.PreferenceScreen +import com.android.settingslib.datastore.KeyedDataObservable +import com.android.settingslib.datastore.KeyedObservable +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceHierarchy +import com.android.settingslib.metadata.PreferenceLifecycleProvider +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.PreferenceScreenRegistry +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableMultimap +import java.util.concurrent.Executor + +/** + * Helper to bind preferences on given [preferenceScreen]. + * + * When there is any preference change event detected (e.g. preference value changed, runtime + * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update + * widget UI. + */ +class PreferenceScreenBindingHelper( + context: Context, + private val preferenceBindingFactory: PreferenceBindingFactory, + private val preferenceScreen: PreferenceScreen, + preferenceHierarchy: PreferenceHierarchy, +) : KeyedDataObservable<String>(), AutoCloseable { + + private val handler = Handler(Looper.getMainLooper()) + private val executor = + object : Executor { + override fun execute(command: Runnable) { + handler.post(command) + } + } + + private val preferences: ImmutableMap<String, PreferenceMetadata> + private val dependencies: ImmutableMultimap<String, String> + private val storages = mutableSetOf<KeyedObservable<String>>() + + private val preferenceObserver: KeyedObserver<String?> + + private val storageObserver = + KeyedObserver<String?> { key, _ -> + if (key != null) { + notifyChange(key, CHANGE_REASON_VALUE) + } + } + + private val stateObserver = + object : PreferenceLifecycleProvider.PreferenceStateObserver { + override fun onPreferenceStateChanged(preference: PreferenceMetadata) { + notifyChange(preference.key, CHANGE_REASON_STATE) + } + } + + init { + val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>() + val dependenciesBuilder = ImmutableMultimap.builder<String, String>() + fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) { + dependenciesBuilder.put(key, dependency.key) + } + + fun PreferenceMetadata.add() { + preferencesBuilder.put(key, this) + dependencyOfEnabledState(context)?.addDependency(this) + if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver) + if (this is PersistentPreference<*>) storages.add(storage(context)) + } + + fun PreferenceHierarchy.addPreferences() { + metadata.add() + forEach { + if (it is PreferenceHierarchy) { + it.addPreferences() + } else { + it.metadata.add() + } + } + } + + preferenceHierarchy.addPreferences() + this.preferences = preferencesBuilder.buildOrThrow() + this.dependencies = dependenciesBuilder.build() + + preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) } + addObserver(preferenceObserver, executor) + for (storage in storages) storage.addObserver(storageObserver, executor) + } + + private fun onPreferenceChange(key: String?, reason: Int) { + if (key == null) return + + // bind preference to update UI + preferenceScreen.findPreference<Preference>(key)?.let { + preferenceBindingFactory.bind(it, preferences[key]) + } + + // check reason to avoid potential infinite loop + if (reason != CHANGE_REASON_DEPENDENT) { + notifyDependents(key, mutableSetOf()) + } + } + + /** Notifies dependents recursively. */ + private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) { + if (!notifiedKeys.add(key)) return + for (dependency in dependencies[key]) { + notifyChange(dependency, CHANGE_REASON_DEPENDENT) + notifyDependents(dependency, notifiedKeys) + } + } + + override fun close() { + removeObserver(preferenceObserver) + val context = preferenceScreen.context + for (preference in preferences.values) { + if (preference is PreferenceLifecycleProvider) preference.onDetach(context) + } + for (storage in storages) storage.removeObserver(storageObserver) + } + + companion object { + /** Preference value is changed. */ + private const val CHANGE_REASON_VALUE = 0 + /** Preference state (title/summary, enable state, etc.) is changed. */ + private const val CHANGE_REASON_STATE = 1 + /** Dependent preference state is changed. */ + private const val CHANGE_REASON_DEPENDENT = 2 + + /** Updates preference screen that has incomplete hierarchy. */ + @JvmStatic + fun bind(preferenceScreen: PreferenceScreen) { + PreferenceScreenRegistry[preferenceScreen.key]?.run { + if (!hasCompleteHierarchy()) { + val preferenceBindingFactory = + (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return + bindRecursively( + preferenceScreen, + preferenceBindingFactory, + getPreferenceHierarchy(preferenceScreen.context), + ) + } + } + } + + internal fun bindRecursively( + preferenceScreen: PreferenceScreen, + preferenceBindingFactory: PreferenceBindingFactory, + preferenceHierarchy: PreferenceHierarchy, + ) = + preferenceScreen.bindRecursively( + preferenceBindingFactory, + preferenceHierarchy.getAllPreferences().associateBy { it.key }, + ) + + private fun PreferenceGroup.bindRecursively( + preferenceBindingFactory: PreferenceBindingFactory, + preferences: Map<String, PreferenceMetadata>, + ) { + preferenceBindingFactory.bind(this, preferences[key]) + val count = preferenceCount + for (index in 0 until count) { + val preference = getPreference(index) + if (preference is PreferenceGroup) { + preference.bindRecursively(preferenceBindingFactory, preferences) + } else { + preferenceBindingFactory.bind(preference, preferences[preference.key]) + } + } + } + + private fun PreferenceBindingFactory.bind( + preference: Preference, + metadata: PreferenceMetadata?, + ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) } + } +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt new file mode 100644 index 000000000000..7f99d7a9bbdd --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.preference.PreferenceScreen +import com.android.settingslib.metadata.PreferenceScreenRegistry + +/** Factory to create preference screen. */ +class PreferenceScreenFactory { + /** Preference manager to create/inflate preference screen. */ + val preferenceManager: PreferenceManager + + /** + * Optional existing hierarchy to merge the new hierarchies into. + * + * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for + * [PreferenceFragmentCompat]. + */ + private val rootScreen: PreferenceScreen? + + /** + * Factory constructor from preference fragment. + * + * The fragment must be within a valid lifecycle. + */ + constructor(preferenceFragment: PreferenceFragmentCompat) { + preferenceManager = preferenceFragment.preferenceManager + rootScreen = preferenceFragment.preferenceScreen + } + + /** Factory constructor from [Context]. */ + constructor(context: Context) : this(PreferenceManager(context)) + + /** Factory constructor from [PreferenceManager]. */ + constructor(preferenceManager: PreferenceManager) { + this.preferenceManager = preferenceManager + rootScreen = null + } + + /** Context of the factory to create preference screen. */ + val context: Context + get() = preferenceManager.context + + /** Returns the existing hierarchy or create a new empty preference screen. */ + fun getOrCreatePreferenceScreen(): PreferenceScreen = + rootScreen ?: preferenceManager.createPreferenceScreen(context) + + /** + * Inflates [PreferenceScreen] from xml resource. + * + * @param xmlRes The resource ID of the XML to inflate + * @return The root hierarchy (if one was not provided, the new hierarchy's root) + */ + fun inflate(xmlRes: Int): PreferenceScreen? = + if (xmlRes != 0) { + preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen) + } else { + rootScreen + } + + /** + * Creates [PreferenceScreen] of given key. + * + * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy. + */ + fun createBindingScreen(screenKey: String?): PreferenceScreen? { + val metadata = PreferenceScreenRegistry[screenKey] ?: return null + if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) { + return metadata.createPreferenceScreen(this) + } + return null + } + + companion object { + /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */ + @JvmStatic + fun createBindingScreen(preference: Preference): PreferenceScreen? { + val preferenceScreenCreator = + (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator) + ?: return null + if (!preferenceScreenCreator.hasCompleteHierarchy()) return null + val factory = PreferenceScreenFactory(preference.context) + val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory) + factory.preferenceManager.setPreferences(preferenceScreen) + return preferenceScreen + } + } +} diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt new file mode 100644 index 000000000000..057329293796 --- /dev/null +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.preference + +import android.content.Context +import androidx.preference.PreferenceScreen + +/** + * Interface to provide [PreferenceScreen]. + * + * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`, + * `getActivity()`) MUST not be used: preference screen creation could happen in background service, + * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked + * and context APIs return null. + */ +interface PreferenceScreenProvider { + + /** + * Creates [PreferenceScreen]. + * + * Preference screen creation could happen in background service. The implementation MUST use + * [PreferenceScreenFactory.context] to obtain context. + */ + fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index 90cee163f8f3..1f3e24254027 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -25,6 +25,13 @@ object SettingsDimension { val paddingLarge = 16.dp val paddingExtraLarge = 24.dp + val spinnerHorizontalPadding = paddingExtraLarge + val spinnerVerticalPadding = paddingLarge + + val actionIconWidth = 32.dp + val actionIconHeight = 40.dp + val actionIconPadding = 4.dp + val itemIconSize = 24.dp val itemIconContainerSize = 72.dp val itemPaddingStart = paddingExtraLarge diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt index 5f320f7ade3f..9bbc16d56811 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt @@ -17,15 +17,24 @@ package com.android.settingslib.spa.widget.scaffold import androidx.appcompat.R +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.FindInPage import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsShape +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled /** Action that navigates back to last page. */ @Composable @@ -50,6 +59,11 @@ private fun BackAction(contentDescription: String, onClick: () -> Unit) { Icon( imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = contentDescription, + modifier = if (isSpaExpressiveEnabled) Modifier + .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight) + .clip(SettingsShape.CornerExtraLarge) + .background(MaterialTheme.colorScheme.onSurfaceVariant) + .padding(SettingsDimension.actionIconPadding) else Modifier ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index 4cf741e517be..7d8ee79b3344 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.compose.horizontalValues import com.android.settingslib.spa.framework.compose.verticalValues import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled import com.android.settingslib.spa.framework.theme.settingsBackground import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel @@ -55,6 +56,10 @@ fun SettingsScaffold( ) { ActivityTitle(title) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + if (isSpaExpressiveEnabled) { + LaunchedEffect(scrollBehavior.state.heightOffsetLimit) { scrollBehavior.collapse() } + } + Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { SettingsTopAppBar(title, scrollBehavior, actions) }, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt index c48a1479555f..6b2db90c6b1f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled data class SpinnerOption( val id: Int, @@ -70,7 +71,10 @@ fun Spinner(options: List<SpinnerOption>, selectedId: Int?, setId: (id: Int) -> ) .selectableGroup(), ) { - val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd) + val contentPadding = if (isSpaExpressiveEnabled) PaddingValues( + horizontal = SettingsDimension.spinnerHorizontalPadding, + vertical = SettingsDimension.spinnerVerticalPadding + ) else PaddingValues(horizontal = SettingsDimension.itemPaddingEnd) Button( modifier = Modifier.semantics { role = Role.DropdownList }, onClick = { expanded = true }, @@ -129,7 +133,11 @@ private fun SpinnerText( text = option?.text ?: "", modifier = modifier .padding(end = SettingsDimension.itemPaddingEnd) - .padding(vertical = SettingsDimension.itemPaddingAround), + .then( + if (!isSpaExpressiveEnabled) + Modifier.padding(vertical = SettingsDimension.itemPaddingAround) + else Modifier + ), color = color, style = MaterialTheme.typography.labelLarge, ) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt new file mode 100644 index 000000000000..65adec4a71a8 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.bluetooth.devicesettings + +/** The contract between the device settings provider services and Settings. */ +object DeviceSettingContract { + const val INVISIBLE_PROFILES = "INVISIBLE_PROFILES" +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt index 457d6a3a714d..769b6e6796f9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt @@ -22,6 +22,7 @@ import android.text.TextUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference import com.android.settingslib.bluetooth.devicesettings.DeviceSetting +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingContract import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig @@ -30,6 +31,9 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceSettingHelpPrefere import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference import com.android.settingslib.bluetooth.devicesettings.ToggleInfo import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.AppProvidedItem +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel @@ -103,9 +107,18 @@ class DeviceSettingRepositoryImpl( private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel { return if (!TextUtils.isEmpty(preferenceKey)) { - DeviceSettingConfigItemModel.BuiltinItem(settingId, preferenceKey!!) + if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) { + BluetoothProfilesItem( + settingId, + preferenceKey!!, + extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES) + ?: emptyList() + ) + } else { + CommonBuiltinItem(settingId, preferenceKey!!) + } } else { - DeviceSettingConfigItemModel.AppProvidedItem(settingId) + AppProvidedItem(settingId) } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt index 33beb06e2ed5..7eae5b2a1f5f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import android.os.IInterface +import android.text.TextUtils import android.util.Log import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice @@ -84,6 +85,10 @@ class DeviceSettingServiceConnection( } setAction(intentAction) } + + fun isValid(): Boolean { + return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction) + } } private var isServiceEnabled = @@ -96,7 +101,8 @@ class DeviceSettingServiceConnection( } else if (allStatus.all { it is ServiceConnectionStatus.Connected }) { allStatus .filterIsInstance< - ServiceConnectionStatus.Connected<IDeviceSettingsProviderService> + ServiceConnectionStatus.Connected< + IDeviceSettingsProviderService> >() .all { it.service.serviceStatus?.enabled == true } } else { @@ -215,6 +221,7 @@ class DeviceSettingServiceConnection( ) } } + ?.filter { it.isValid() } ?.distinct() ?.associateBy( { it }, diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt index c1ac763929cd..08fb3fb8fb22 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt @@ -36,10 +36,23 @@ sealed interface DeviceSettingConfigItemModel { @DeviceSettingId val settingId: Int /** A built-in item in Settings. */ - data class BuiltinItem( - @DeviceSettingId override val settingId: Int, - val preferenceKey: String? - ) : DeviceSettingConfigItemModel + sealed interface BuiltinItem : DeviceSettingConfigItemModel { + @DeviceSettingId override val settingId: Int + val preferenceKey: String + + /** A general built-in item in Settings. */ + data class CommonBuiltinItem( + @DeviceSettingId override val settingId: Int, + override val preferenceKey: String, + ) : BuiltinItem + + /** A bluetooth profiles in Settings. */ + data class BluetoothProfilesItem( + @DeviceSettingId override val settingId: Int, + override val preferenceKey: String, + val invisibleProfiles: List<String>, + ) : BuiltinItem + } /** A remote item provided by other apps. */ data class AppProvidedItem(@DeviceSettingId override val settingId: Int) : diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 043219a65da4..c686708a3c18 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -61,6 +61,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value += zenModes } + fun addMode(mode: ZenMode) { + mutableModesFlow.value += mode + } + fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, active: Boolean = false) { mutableModesFlow.value += newMode(id, type, active) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index ce155b5c0fa4..81b56343ceed 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -91,7 +91,9 @@ class DeviceSettingRepositoryTest { `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS) `when`( bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS + ) + ) .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray()) `when`(configService.queryLocalInterface(anyString())).thenReturn(configService) @@ -114,7 +116,8 @@ class DeviceSettingRepositoryTest { connection.onServiceConnected( ComponentName( SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1, - SETTING_PROVIDER_SERVICE_CLASS_NAME_1), + SETTING_PROVIDER_SERVICE_CLASS_NAME_1, + ), settingProviderService1, ) SETTING_PROVIDER_SERVICE_INTENT_ACTION_2 -> @@ -146,16 +149,24 @@ class DeviceSettingRepositoryTest { fun getDeviceSettingsConfig_withMetadata_success() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) val config = underTest.getDeviceSettingsConfig(cachedDevice) assertConfig(config!!, DEVICE_SETTING_CONFIG) + assertThat(config.mainItems[0]) + .isInstanceOf(DeviceSettingConfigItemModel.AppProvidedItem::class.java) + assertThat(config.mainItems[1]) + .isInstanceOf( + DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem::class.java + ) + assertThat(config.mainItems[2]) + .isInstanceOf( + DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem::class.java + ) } } @@ -163,16 +174,16 @@ class DeviceSettingRepositoryTest { fun getDeviceSettingsConfig_noMetadata_returnNull() { testScope.runTest { `when`( - bluetoothDevice.getMetadata( - DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)) + bluetoothDevice.getMetadata( + DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS + ) + ) .thenReturn("".toByteArray()) `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -184,12 +195,10 @@ class DeviceSettingRepositoryTest { fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(false) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(false)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) val config = underTest.getDeviceSettingsConfig(cachedDevice) @@ -219,12 +228,10 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) var setting: DeviceSettingModel? = null underTest @@ -247,12 +254,10 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) var setting: DeviceSettingModel? = null underTest @@ -270,17 +275,15 @@ class DeviceSettingRepositoryTest { testScope.runTest { `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG) `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then { - input -> + input -> input .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP)) } - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) var setting: DeviceSettingModel? = null underTest @@ -324,12 +327,10 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1)) } - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) var setting: DeviceSettingModel? = null underTest @@ -347,8 +348,10 @@ class DeviceSettingRepositoryTest { DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) .setPreferenceState( - ActionSwitchPreferenceState.Builder().setChecked(false).build()) - .build()) + ActionSwitchPreferenceState.Builder().setChecked(false).build() + ) + .build(), + ) } } @@ -362,12 +365,10 @@ class DeviceSettingRepositoryTest { .getArgument<IDeviceSettingsListener>(1) .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2)) } - `when`(settingProviderService1.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) - `when`(settingProviderService2.serviceStatus).thenReturn( - DeviceSettingsProviderServiceStatus(true) - ) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) var setting: DeviceSettingModel? = null underTest @@ -385,8 +386,10 @@ class DeviceSettingRepositoryTest { DeviceSettingState.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC) .setPreferenceState( - MultiTogglePreferenceState.Builder().setState(2).build()) - .build()) + MultiTogglePreferenceState.Builder().setState(2).build() + ) + .build(), + ) } } @@ -437,7 +440,7 @@ class DeviceSettingRepositoryTest { private fun assertConfig( actual: DeviceSettingConfigModel, - serviceResponse: DeviceSettingsConfig + serviceResponse: DeviceSettingsConfig, ) { assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size) for (i in 0..<actual.mainItems.size) { @@ -451,7 +454,7 @@ class DeviceSettingRepositoryTest { private fun assertConfigItem( actual: DeviceSettingConfigItemModel, - serviceResponse: DeviceSettingItem + serviceResponse: DeviceSettingItem, ) { assertThat(actual.settingId).isEqualTo(serviceResponse.settingId) } @@ -485,24 +488,43 @@ class DeviceSettingRepositoryTest { "</DEVICE_SETTINGS_CONFIG_ACTION>" val DEVICE_INFO = DeviceInfo.Builder().setBluetoothAddress(BLUETOOTH_ADDRESS).build() const val DEVICE_SETTING_ID_HELP = 12345 - val DEVICE_SETTING_ITEM_1 = + val DEVICE_SETTING_APP_PROVIDED_ITEM_1 = DeviceSettingItem( DeviceSettingId.DEVICE_SETTING_ID_HEADER, SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1, SETTING_PROVIDER_SERVICE_CLASS_NAME_1, - SETTING_PROVIDER_SERVICE_INTENT_ACTION_1) - val DEVICE_SETTING_ITEM_2 = + SETTING_PROVIDER_SERVICE_INTENT_ACTION_1, + ) + val DEVICE_SETTING_APP_PROVIDED_ITEM_2 = DeviceSettingItem( DeviceSettingId.DEVICE_SETTING_ID_ANC, SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2, SETTING_PROVIDER_SERVICE_CLASS_NAME_2, - SETTING_PROVIDER_SERVICE_INTENT_ACTION_2) + SETTING_PROVIDER_SERVICE_INTENT_ACTION_2, + ) + val DEVICE_SETTING_BUILT_IN_ITEM = + DeviceSettingItem( + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP, + "", + "", + "", + "device_type", + ) + val DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM = + DeviceSettingItem( + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, + "", + "", + "", + "bluetooth_profiles", + ) val DEVICE_SETTING_HELP_ITEM = DeviceSettingItem( DEVICE_SETTING_ID_HELP, SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2, SETTING_PROVIDER_SERVICE_CLASS_NAME_2, - SETTING_PROVIDER_SERVICE_INTENT_ACTION_2) + SETTING_PROVIDER_SERVICE_INTENT_ACTION_2, + ) val DEVICE_SETTING_1 = DeviceSetting.Builder() .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER) @@ -511,7 +533,8 @@ class DeviceSettingRepositoryTest { .setTitle("title1") .setHasSwitch(true) .setAllowedChangingState(true) - .build()) + .build() + ) .build() val DEVICE_SETTING_2 = DeviceSetting.Builder() @@ -524,22 +547,30 @@ class DeviceSettingRepositoryTest { ToggleInfo.Builder() .setLabel("label1") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) - .build()) + .build() + ) .addToggleInfo( ToggleInfo.Builder() .setLabel("label2") .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) - .build()) - .build()) + .build() + ) + .build() + ) + .build() + val DEVICE_SETTING_HELP = + DeviceSetting.Builder() + .setSettingId(DEVICE_SETTING_ID_HELP) + .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build()) .build() - val DEVICE_SETTING_HELP = DeviceSetting.Builder() - .setSettingId(DEVICE_SETTING_ID_HELP) - .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build()) - .build() val DEVICE_SETTING_CONFIG = DeviceSettingsConfig( - listOf(DEVICE_SETTING_ITEM_1), - listOf(DEVICE_SETTING_ITEM_2), + listOf( + DEVICE_SETTING_APP_PROVIDED_ITEM_1, + DEVICE_SETTING_BUILT_IN_ITEM, + DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM, + ), + listOf(DEVICE_SETTING_APP_PROVIDED_ITEM_2), DEVICE_SETTING_HELP_ITEM, ) } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index d26a9066e075..a9e81c77acad 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -756,6 +756,7 @@ android_library { "notification_flags_lib", "PlatformComposeCore", "PlatformComposeSceneTransitionLayout", + "PlatformComposeSceneTransitionLayoutTestsUtils", "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", "androidx.compose.material_material-icons-extended", diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_assistant_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_assistant.xml index 9c3417fde80a..6408a122a99e 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_assistant_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_assistant.xml @@ -1,6 +1,6 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_down_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_down.xml index a64a0d18acd1..f13239cf78fb 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_down_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_down.xml @@ -1,6 +1,6 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_up_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_up.xml index 40423c7c35dd..a5d15f96759f 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_up_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_brightness_up.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_lock_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_lock.xml index a0f7b5d04379..212763283718 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_lock_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_lock.xml @@ -1,6 +1,6 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_notifications_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_notifications.xml index 8757f22f2af6..62c8d1ddaba2 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_notifications_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_notifications.xml @@ -1,6 +1,6 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_power_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_power.xml index 049013aa9763..ed11b44cc25a 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_power_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_power.xml @@ -1,6 +1,6 @@ <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_quick_settings_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_quick_settings.xml index 4f25e7d80bd6..2da63a61e84e 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_quick_settings_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_quick_settings.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_recent_apps_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_recent_apps.xml index 38234c05d591..9763b8eefbe2 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_recent_apps_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_recent_apps.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_screenshot_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_screenshot.xml index 6d7f49cf6919..2bfbd5b48991 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_screenshot_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_screenshot.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_settings_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_settings.xml index 5ed6f19051bc..4ca9bfc7900d 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_settings_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_settings.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_down_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_down.xml index 16653e8e9d3e..f924e5eb9493 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_down_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_down.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_up_24dp.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_up.xml index e572c6adea33..41fe35184a2a 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_up_24dp.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/ic_logo_a11y_volume_up.xml @@ -15,8 +15,8 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" + android:width="108dp" + android:height="108dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="@color/colorControlNormal"> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/menuitem_background_ripple.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/menuitem_background_ripple.xml new file mode 100644 index 000000000000..6cab464f0a9c --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/drawable/menuitem_background_ripple.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open 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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/ripple_material_color" />
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml index 3c73eca732a1..a1130e6e2285 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml @@ -12,7 +12,8 @@ android:layout_height="@dimen/image_button_height" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" - android:scaleType="fitCenter"/> + android:scaleType="fitCenter" + android:background="@drawable/menuitem_background_ripple" /> <TextView android:id="@+id/shortcutLabel" diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java index c698d18bfde8..11ce41ee4d96 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java @@ -53,73 +53,73 @@ public class A11yMenuShortcut { /** Map stores all shortcut resource IDs that is in matching order of defined shortcut. */ private static final Map<ShortcutId, int[]> sShortcutResource = Map.ofEntries( Map.entry(ShortcutId.ID_ASSISTANT_VALUE, new int[] { - R.drawable.ic_logo_a11y_assistant_24dp, + R.drawable.ic_logo_a11y_assistant, R.color.assistant_color, R.string.assistant_utterance, R.string.assistant_label, }), Map.entry(ShortcutId.ID_A11YSETTING_VALUE, new int[] { - R.drawable.ic_logo_a11y_settings_24dp, + R.drawable.ic_logo_a11y_settings, R.color.a11y_settings_color, R.string.a11y_settings_label, R.string.a11y_settings_label, }), Map.entry(ShortcutId.ID_POWER_VALUE, new int[] { - R.drawable.ic_logo_a11y_power_24dp, + R.drawable.ic_logo_a11y_power, R.color.power_color, R.string.power_utterance, R.string.power_label, }), Map.entry(ShortcutId.ID_RECENT_VALUE, new int[] { - R.drawable.ic_logo_a11y_recent_apps_24dp, + R.drawable.ic_logo_a11y_recent_apps, R.color.recent_apps_color, R.string.recent_apps_label, R.string.recent_apps_label, }), Map.entry(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] { - R.drawable.ic_logo_a11y_lock_24dp, + R.drawable.ic_logo_a11y_lock, R.color.lockscreen_color, R.string.lockscreen_label, R.string.lockscreen_label, }), Map.entry(ShortcutId.ID_QUICKSETTING_VALUE, new int[] { - R.drawable.ic_logo_a11y_quick_settings_24dp, + R.drawable.ic_logo_a11y_quick_settings, R.color.quick_settings_color, R.string.quick_settings_label, R.string.quick_settings_label, }), Map.entry(ShortcutId.ID_NOTIFICATION_VALUE, new int[] { - R.drawable.ic_logo_a11y_notifications_24dp, + R.drawable.ic_logo_a11y_notifications, R.color.notifications_color, R.string.notifications_label, R.string.notifications_label, }), Map.entry(ShortcutId.ID_SCREENSHOT_VALUE, new int[] { - R.drawable.ic_logo_a11y_screenshot_24dp, + R.drawable.ic_logo_a11y_screenshot, R.color.screenshot_color, R.string.screenshot_utterance, R.string.screenshot_label, }), Map.entry(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] { - R.drawable.ic_logo_a11y_brightness_up_24dp, + R.drawable.ic_logo_a11y_brightness_up, R.color.brightness_color, R.string.brightness_up_label, R.string.brightness_up_label, }), Map.entry(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] { - R.drawable.ic_logo_a11y_brightness_down_24dp, + R.drawable.ic_logo_a11y_brightness_down, R.color.brightness_color, R.string.brightness_down_label, R.string.brightness_down_label, }), Map.entry(ShortcutId.ID_VOLUME_UP_VALUE, new int[] { - R.drawable.ic_logo_a11y_volume_up_24dp, + R.drawable.ic_logo_a11y_volume_up, R.color.volume_color, R.string.volume_up_label, R.string.volume_up_label, }), Map.entry(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] { - R.drawable.ic_logo_a11y_volume_down_24dp, + R.drawable.ic_logo_a11y_volume_down, R.color.volume_color, R.string.volume_down_label, R.string.volume_down_label, diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java deleted file mode 100644 index 28ba4b54107f..000000000000 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/utils/ShortcutDrawableUtils.java +++ /dev/null @@ -1,98 +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.accessibility.accessibilitymenu.utils; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.RippleDrawable; - -import com.android.systemui.accessibility.accessibilitymenu.R; - -/** Creates background drawable for a11y menu shortcut. */ -public class ShortcutDrawableUtils { - - /** - * To make the circular background of shortcut icons have higher resolution. The higher value of - * LENGTH is, the higher resolution of the circular background are. - */ - private static final int LENGTH = 480; - - private static final int RADIUS = LENGTH / 2; - private static final int COORDINATE = LENGTH / 2; - private static final int RIPPLE_COLOR_ID = R.color.ripple_material_color; - - private final Context mContext; - private final ColorStateList mRippleColorStateList; - - // Placeholder of drawable to prevent NullPointerException - private final ColorDrawable mTransparentDrawable = new ColorDrawable(Color.TRANSPARENT); - - public ShortcutDrawableUtils(Context context) { - this.mContext = context; - - int rippleColor = context.getColor(RIPPLE_COLOR_ID); - mRippleColorStateList = ColorStateList.valueOf(rippleColor); - } - - /** - * Creates a circular drawable in specific color for shortcut. - * - * @param colorResId color resource ID - * @return drawable circular drawable - */ - public Drawable createCircularDrawable(int colorResId) { - Bitmap output = Bitmap.createBitmap(LENGTH, LENGTH, Config.ARGB_8888); - Canvas canvas = new Canvas(output); - int color = mContext.getColor(colorResId); - Paint paint = new Paint(); - paint.setColor(color); - paint.setStrokeCap(Paint.Cap.ROUND); - paint.setStyle(Style.FILL); - canvas.drawCircle(COORDINATE, COORDINATE, RADIUS, paint); - - BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), output); - return drawable; - } - - /** - * Creates an adaptive icon drawable in specific color for shortcut. - * - * @param colorResId color resource ID - * @return drawable for adaptive icon - */ - public Drawable createAdaptiveIconDrawable(int colorResId) { - Drawable circleLayer = createCircularDrawable(colorResId); - RippleDrawable rippleLayer = new RippleDrawable(mRippleColorStateList, null, null); - - AdaptiveIconDrawable adaptiveIconDrawable = - new AdaptiveIconDrawable(circleLayer, mTransparentDrawable); - - Drawable[] layers = {adaptiveIconDrawable, rippleLayer}; - LayerDrawable drawable = new LayerDrawable(layers); - return drawable; - } -} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java index c333a7a5e33e..aa1bbbdada65 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 @@ -16,7 +16,12 @@ package com.android.systemui.accessibility.accessibilitymenu.view; +import android.content.res.Resources; import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.view.LayoutInflater; import android.view.TouchDelegate; import android.view.View; @@ -26,11 +31,12 @@ import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; import com.android.systemui.accessibility.accessibilitymenu.R; import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; -import com.android.systemui.accessibility.accessibilitymenu.utils.ShortcutDrawableUtils; import java.util.List; @@ -43,16 +49,12 @@ public class A11yMenuAdapter extends BaseAdapter { private final AccessibilityMenuService mService; private final List<A11yMenuShortcut> mShortcutDataList; - private final ShortcutDrawableUtils mShortcutDrawableUtils; public A11yMenuAdapter( AccessibilityMenuService service, List<A11yMenuShortcut> shortcutDataList) { this.mService = service; this.mShortcutDataList = shortcutDataList; - - mShortcutDrawableUtils = new ShortcutDrawableUtils(service); - mLargeTextSize = service.getResources().getDimensionPixelOffset(R.dimen.large_label_text_size); } @@ -152,10 +154,10 @@ public class A11yMenuAdapter extends BaseAdapter { shortcutIconButton.setContentDescription( mService.getString(shortcutItem.imgContentDescription)); shortcutLabel.setText(shortcutItem.labelText); - shortcutIconButton.setImageResource(shortcutItem.imageSrc); - shortcutIconButton.setBackground( - mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor)); + AdaptiveIconDrawable iconDrawable = getAdaptiveIconDrawable(convertView, + shortcutItem); + shortcutIconButton.setImageDrawable(iconDrawable); shortcutIconButton.setAccessibilityDelegate(new View.AccessibilityDelegate() { @Override @@ -167,4 +169,18 @@ public class A11yMenuAdapter extends BaseAdapter { }); } } + + @NonNull + private static AdaptiveIconDrawable getAdaptiveIconDrawable(@NonNull View convertView, + @NonNull A11yMenuShortcut shortcutItem) { + Resources resources = convertView.getResources(); + // Note: from the official guide, the foreground image of the adaptive icon should be + // sized at 108 x 108 dp + Drawable icon = resources.getDrawable(shortcutItem.imageSrc); + float inset = AdaptiveIconDrawable.getExtraInsetFraction(); + AdaptiveIconDrawable iconDrawable = new AdaptiveIconDrawable( + new ColorDrawable(resources.getColor(shortcutItem.imageColor)), + new InsetDrawable(icon, inset)); + return iconDrawable; + } } diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java index 35f124874597..b899c45b0f7e 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java @@ -306,6 +306,10 @@ public class A11yMenuViewPager { (viewPagerHeight - topMargin - defaultMargin - (rowsInGridView * gridItemHeight)) / (rowsInGridView + 1); + // The interval is negative number when the viewPagerHeight is not able to fit + // the grid items, which result in text overlapping. + // Adjust the interval to 0 could solve the issue. + interval = Math.max(interval, 0); mViewPagerAdapter.setVerticalSpacing(interval); // Sets padding to view pager. diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index d1a59af1c471..ad14035d9d0a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -288,6 +288,16 @@ flag { } flag { + name: "qs_quick_rebind_active_tiles" + namespace: "systemui" + description: "Rebind active custom tiles quickly." + bug: "362526228" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "coroutine_tracing" namespace: "systemui" description: "Adds thread-local data to System UI's global coroutine scopes to " @@ -606,16 +616,6 @@ flag { } flag { - name: "screenshot_save_image_exporter" - namespace: "systemui" - description: "Save all screenshots using ImageExporter" - bug: "352308052" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "screenshot_ui_controller_refactor" namespace: "systemui" description: "Simplify and refactor ScreenshotController" @@ -1396,3 +1396,9 @@ flag { } } +flag { + name: "non_touchscreen_devices_bypass_falsing" + namespace: "systemui" + description: "Allow non-touchscreen devices to bypass falsing" + bug: "319809270" +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 7fb88e8d1fcc..ae92d259d62b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -99,8 +99,8 @@ private fun SceneScope.BouncerScene( BouncerContent( viewModel, dialogFactory, - Modifier.sysuiResTag(Bouncer.TestTags.Root) - .element(Bouncer.Elements.Content) + Modifier.element(Bouncer.Elements.Content) + .sysuiResTag(Bouncer.TestTags.Root) .fillMaxSize() ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index bcbf933d9370..c63b29dd9051 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -137,6 +137,7 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign @@ -241,10 +242,15 @@ fun CommunalHub( } } + val paneTitle = stringResource(R.string.accessibility_content_description_for_communal_hub) + Box( modifier = modifier - .semantics { testTagsAsResourceId = true } + .semantics { + testTagsAsResourceId = true + this.paneTitle = paneTitle + } .testTag(COMMUNAL_HUB_TEST_TAG) .fillMaxSize() // Observe taps for selecting items diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt index ecb3d8cb04be..c25a45dc5cf6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt @@ -52,6 +52,7 @@ import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel +import com.android.systemui.log.LongPressHandlingViewLogger import com.android.systemui.res.R import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -97,6 +98,7 @@ fun AlternateBouncer( Box { DeviceEntryIcon( viewModel = alternateBouncerDependencies.udfpsIconViewModel, + logger = alternateBouncerDependencies.logger, modifier = Modifier.width { udfpsLocation.width } .height { udfpsLocation.height } @@ -151,13 +153,14 @@ private fun StatusMessage( @Composable private fun DeviceEntryIcon( viewModel: AlternateBouncerUdfpsIconViewModel, + logger: LongPressHandlingViewLogger, modifier: Modifier = Modifier, ) { AndroidView( modifier = modifier, factory = { context -> val view = - DeviceEntryIconView(context, null).apply { + DeviceEntryIconView(context, null, logger = logger).apply { id = R.id.alternate_bouncer_udfps_icon_view contentDescription = context.resources.getString(R.string.accessibility_fingerprint_label) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 46cd58ce6dd0..a525f36c71ce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -44,6 +44,9 @@ import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LongPressHandlingViewLogger +import com.android.systemui.log.dagger.LongPressTouchLog import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper @@ -64,6 +67,7 @@ constructor( private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>, private val falsingManager: Lazy<FalsingManager>, private val vibratorHelper: Lazy<VibratorHelper>, + @LongPressTouchLog private val logBuffer: LogBuffer, ) { @Composable fun SceneScope.LockIcon(overrideColor: Color? = null, modifier: Modifier = Modifier) { @@ -77,19 +81,24 @@ constructor( factory = { context -> val view = if (DeviceEntryUdfpsRefactor.isEnabled) { - DeviceEntryIconView(context, null).apply { - id = R.id.device_entry_icon_view - DeviceEntryIconViewBinder.bind( - applicationScope, - this, - deviceEntryIconViewModel.get(), - deviceEntryForegroundViewModel.get(), - deviceEntryBackgroundViewModel.get(), - falsingManager.get(), - vibratorHelper.get(), - overrideColor, + DeviceEntryIconView( + context, + null, + logger = LongPressHandlingViewLogger(logBuffer, tag = TAG) ) - } + .apply { + id = R.id.device_entry_icon_view + DeviceEntryIconViewBinder.bind( + applicationScope, + this, + deviceEntryIconViewModel.get(), + deviceEntryForegroundViewModel.get(), + deviceEntryBackgroundViewModel.get(), + falsingManager.get(), + vibratorHelper.get(), + overrideColor, + ) + } } else { // KeyguardBottomAreaRefactor.isEnabled LockIconView(context, null).apply { @@ -178,6 +187,10 @@ constructor( return IntRect(center, radius) } + + companion object { + private const val TAG = "LockSection" + } } private val LockIconElementKey = ElementKey("LockIcon") diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 3cb0d8af1ba4..df101c558dff 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -128,7 +128,11 @@ fun SceneContainer( } }, ) { - SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) { + SceneTransitionLayout( + state = state, + modifier = modifier.fillMaxSize(), + swipeSourceDetector = viewModel.edgeDetector, + ) { sceneByKey.forEach { (sceneKey, scene) -> scene( key = sceneKey, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 24fef711d397..007b84a2954a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -190,14 +190,12 @@ internal class DraggableHandlerImpl( private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { val fromSource = startedPosition?.let { position -> - layoutImpl.swipeSourceDetector - .source( - layoutImpl.lastSize, - position.round(), - layoutImpl.density, - orientation, - ) - ?.resolve(layoutImpl.layoutDirection) + layoutImpl.swipeSourceDetector.source( + layoutImpl.lastSize, + position.round(), + layoutImpl.density, + orientation, + ) } val upOrLeft = @@ -397,14 +395,8 @@ private class DragControllerImpl( return 0f } - fun animateTo(targetContent: T) { - swipeAnimation.animateOffset( - initialVelocity = velocity, - targetContent = targetContent, - ) - } - val fromContent = swipeAnimation.fromContent + val consumedVelocity: Float if (canChangeContent) { // If we are halfway between two contents, we check what the target will be based on the // velocity and offset of the transition, then we launch the animation. @@ -429,18 +421,16 @@ private class DragControllerImpl( } else { fromContent } - - animateTo(targetContent = targetContent) + consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = targetContent) } else { // We are doing an overscroll preview animation between scenes. check(fromContent == swipeAnimation.currentContent) { "canChangeContent is false but currentContent != fromContent" } - animateTo(targetContent = fromContent) + consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = fromContent) } - // The onStop animation consumes any remaining velocity. - return velocity + return consumedVelocity } /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt index 97c0cef30388..edd697bd7068 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt @@ -54,23 +54,23 @@ class FixedSizeEdgeDetector(val size: Dp) : SwipeSourceDetector { position: IntOffset, density: Density, orientation: Orientation, - ): Edge? { + ): Edge.Resolved? { val axisSize: Int val axisPosition: Int - val topOrLeft: Edge - val bottomOrRight: Edge + val topOrLeft: Edge.Resolved + val bottomOrRight: Edge.Resolved when (orientation) { Orientation.Horizontal -> { axisSize = layoutSize.width axisPosition = position.x - topOrLeft = Edge.Left - bottomOrRight = Edge.Right + topOrLeft = Edge.Resolved.Left + bottomOrRight = Edge.Resolved.Right } Orientation.Vertical -> { axisSize = layoutSize.height axisPosition = position.y - topOrLeft = Edge.Top - bottomOrRight = Edge.Bottom + topOrLeft = Edge.Resolved.Top + bottomOrRight = Edge.Resolved.Bottom } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 061613f999c6..004bb40bb0ad 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -379,6 +379,10 @@ sealed class UserAction { return this to UserActionResult(toScene = scene) } + infix fun to(overlay: OverlayKey): Pair<UserAction, UserActionResult> { + return this to UserActionResult(toOverlay = overlay) + } + /** Resolve this into a [Resolved] user action given [layoutDirection]. */ internal abstract fun resolve(layoutDirection: LayoutDirection): Resolved @@ -475,7 +479,7 @@ interface SwipeSourceDetector { position: IntOffset, density: Density, orientation: Orientation, - ): SwipeSource? + ): SwipeSource.Resolved? } /** The result of performing a [UserAction]. */ diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 2a09a77788e7..966bda410231 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -312,11 +312,16 @@ internal class SwipeAnimation<T : ContentKey>( fun isAnimatingOffset(): Boolean = offsetAnimation != null + /** + * Animate the offset to a [targetContent], using the [initialVelocity] and an optional [spec] + * + * @return the velocity consumed + */ fun animateOffset( initialVelocity: Float, targetContent: T, spec: AnimationSpec<Float>? = null, - ) { + ): Float { check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" } val initialProgress = progress @@ -374,7 +379,7 @@ internal class SwipeAnimation<T : ContentKey>( if (skipAnimation) { // Unblock the job. offsetAnimationRunnable.complete(null) - return + return 0f } val isTargetGreater = targetOffset > animatable.value @@ -424,6 +429,9 @@ internal class SwipeAnimation<T : ContentKey>( /* Ignore. */ } } + + // This animation always consumes the whole available velocity + return initialVelocity } /** An exception thrown during the animation to stop it immediately. */ diff --git a/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt index 3fda9b85a5c2..41b015a2ede8 100644 --- a/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/systemui/communal/ui/compose/CommunalSwipeDetector.kt @@ -33,7 +33,7 @@ import kotlin.math.abs * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable * hub. */ -class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) : +class CommunalSwipeDetector(private var lastDirection: SwipeSource.Resolved? = null) : SwipeSourceDetector, SwipeDetector { companion object { private const val TRAVEL_RATIO_THRESHOLD = .5f @@ -44,15 +44,15 @@ class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) : position: IntOffset, density: Density, orientation: Orientation - ): SwipeSource? { + ): SwipeSource.Resolved? { return lastDirection } override fun detectSwipe(change: PointerInputChange): Boolean { if (change.positionChange().x > 0) { - lastDirection = Edge.Left + lastDirection = Edge.Resolved.Left } else { - lastDirection = Edge.Right + lastDirection = Edge.Resolved.Right } // Determine whether the ratio of the distance traveled horizontally to the distance diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 79f82c948541..5b5935633166 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -1111,7 +1111,7 @@ class DraggableHandlerTest { assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) // Release the finger. - dragController.onDragStopped(velocity = -velocityThreshold) + dragController.onDragStopped(velocity = -velocityThreshold, expectedConsumed = false) // Exhaust all coroutines *without advancing the clock*. Given that we are at progress >= // 100% and that the overscroll on scene B is doing nothing, we are already idle. diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt index cceaf57ca82b..dea9283c985c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt @@ -34,7 +34,7 @@ class FixedSizeEdgeDetectorTest { @Test fun horizontalEdges() { - fun horizontalEdge(position: Int): Edge? = + fun horizontalEdge(position: Int): Edge.Resolved? = detector.source( layoutSize, position = IntOffset(position, 0), @@ -42,17 +42,17 @@ class FixedSizeEdgeDetectorTest { Orientation.Horizontal, ) - assertThat(horizontalEdge(0)).isEqualTo(Edge.Left) - assertThat(horizontalEdge(30)).isEqualTo(Edge.Left) + assertThat(horizontalEdge(0)).isEqualTo(Edge.Resolved.Left) + assertThat(horizontalEdge(30)).isEqualTo(Edge.Resolved.Left) assertThat(horizontalEdge(31)).isEqualTo(null) assertThat(horizontalEdge(69)).isEqualTo(null) - assertThat(horizontalEdge(70)).isEqualTo(Edge.Right) - assertThat(horizontalEdge(100)).isEqualTo(Edge.Right) + assertThat(horizontalEdge(70)).isEqualTo(Edge.Resolved.Right) + assertThat(horizontalEdge(100)).isEqualTo(Edge.Resolved.Right) } @Test fun verticalEdges() { - fun verticalEdge(position: Int): Edge? = + fun verticalEdge(position: Int): Edge.Resolved? = detector.source( layoutSize, position = IntOffset(0, position), @@ -60,11 +60,11 @@ class FixedSizeEdgeDetectorTest { Orientation.Vertical, ) - assertThat(verticalEdge(0)).isEqualTo(Edge.Top) - assertThat(verticalEdge(30)).isEqualTo(Edge.Top) + assertThat(verticalEdge(0)).isEqualTo(Edge.Resolved.Top) + assertThat(verticalEdge(30)).isEqualTo(Edge.Resolved.Top) assertThat(verticalEdge(31)).isEqualTo(null) assertThat(verticalEdge(69)).isEqualTo(null) - assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom) - assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom) + assertThat(verticalEdge(70)).isEqualTo(Edge.Resolved.Bottom) + assertThat(verticalEdge(100)).isEqualTo(Edge.Resolved.Bottom) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index e2bdc49d590c..bb152086cdab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -30,10 +30,12 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.res.R import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever @@ -89,6 +91,9 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> + private val kosmos = testKosmos() + private val msdlPlayer = kosmos.msdlPlayer + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -112,7 +117,8 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mKeyguardMessageAreaControllerFactory, mPostureController, fakeFeatureFlags, - mSelectedUserInteractor + mSelectedUserInteractor, + msdlPlayer, ) mKeyguardPatternView.onAttachedToWindow() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java index 8f9b7c8cbc45..12c866f0adb2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java @@ -30,11 +30,11 @@ import android.graphics.Point; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.SysuiTestCase; import org.junit.Before; @@ -48,7 +48,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class MirrorWindowControlTest extends SysuiTestCase { - @Mock WindowManager mWindowManager; + @Mock ViewCaptureAwareWindowManager mWindowManager; View mView; int mViewWidth; int mViewHeight; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt index dd85d9bd2d7c..fc57757c9a8c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt @@ -20,11 +20,15 @@ import android.view.accessibility.CaptioningManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.userRepository +import com.android.systemui.user.utils.FakeUserScopedService import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -39,10 +43,11 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@Suppress("UnspecifiedRegisterReceiverFlag") @RunWith(AndroidJUnit4::class) class CaptioningRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Captor private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener> @@ -50,34 +55,33 @@ class CaptioningRepositoryTest : SysuiTestCase() { private lateinit var underTest: CaptioningRepository - private val testScope = TestScope() - @Before fun setup() { MockitoAnnotations.initMocks(this) underTest = - CaptioningRepositoryImpl( - captioningManager, - testScope.testScheduler, - testScope.backgroundScope - ) + with(kosmos) { + CaptioningRepositoryImpl( + FakeUserScopedService(captioningManager), + userRepository, + testScope.testScheduler, + applicationCoroutineScope, + ) + } } @Test fun isSystemAudioCaptioningEnabled_change_repositoryEmits() { - testScope.runTest { - `when`(captioningManager.isEnabled).thenReturn(false) - val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>() - underTest.isSystemAudioCaptioningEnabled - .onEach { isSystemAudioCaptioningEnabled.add(it) } - .launchIn(backgroundScope) + kosmos.testScope.runTest { + `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(false) + val models by collectValues(underTest.captioningModel.filterNotNull()) runCurrent() + `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(true) triggerOnSystemAudioCaptioningChange() runCurrent() - assertThat(isSystemAudioCaptioningEnabled) + assertThat(models.map { it.isSystemAudioCaptioningEnabled }) .containsExactlyElementsIn(listOf(false, true)) .inOrder() } @@ -85,18 +89,16 @@ class CaptioningRepositoryTest : SysuiTestCase() { @Test fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() { - testScope.runTest { - `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false) - val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>() - underTest.isSystemAudioCaptioningUiEnabled - .onEach { isSystemAudioCaptioningUiEnabled.add(it) } - .launchIn(backgroundScope) + kosmos.testScope.runTest { + `when`(captioningManager.isEnabled).thenReturn(false) + val models by collectValues(underTest.captioningModel.filterNotNull()) runCurrent() + `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(true) triggerSystemAudioCaptioningUiChange() runCurrent() - assertThat(isSystemAudioCaptioningUiEnabled) + assertThat(models.map { it.isSystemAudioCaptioningUiEnabled }) .containsExactlyElementsIn(listOf(false, true)) .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java index 09aa286874b9..ca23228459e4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java @@ -17,11 +17,11 @@ package com.android.systemui.accessibility.hearingaid; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.bluetooth.BluetoothDevice; import android.testing.TestableLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -51,6 +51,8 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); + private static final int TEST_LAUNCH_SOURCE_ID = 1; + private final FakeExecutor mMainExecutor = new FakeExecutor(new FakeSystemClock()); private final FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock()); @Mock @@ -70,7 +72,7 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase { @Before public void setUp() { - when(mDialogFactory.create(anyBoolean())).thenReturn(mDialogDelegate); + when(mDialogFactory.create(anyBoolean(), anyInt())).thenReturn(mDialogDelegate); when(mDialogDelegate.createDialog()).thenReturn(mDialog); mManager = new HearingDevicesDialogManager( @@ -86,21 +88,22 @@ public class HearingDevicesDialogManagerTest extends SysuiTestCase { public void showDialog_existHearingDevice_showPairNewDeviceFalse() { when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(true); - mManager.showDialog(mExpandable); + mManager.showDialog(mExpandable, TEST_LAUNCH_SOURCE_ID); mBackgroundExecutor.runAllReady(); mMainExecutor.runAllReady(); - verify(mDialogFactory).create(eq(/* showPairNewDevice= */ false)); + verify(mDialogFactory).create(eq(/* showPairNewDevice= */ false), + eq(TEST_LAUNCH_SOURCE_ID)); } @Test public void showDialog_noHearingDevice_showPairNewDeviceTrue() { when(mDevicesChecker.isAnyPairedHearingDevice()).thenReturn(false); - mManager.showDialog(mExpandable); + mManager.showDialog(mExpandable, TEST_LAUNCH_SOURCE_ID); mBackgroundExecutor.runAllReady(); mMainExecutor.runAllReady(); - verify(mDialogFactory).create(eq(/* showPairNewDevice= */ true)); + verify(mDialogFactory).create(eq(/* showPairNewDevice= */ true), eq(TEST_LAUNCH_SOURCE_ID)); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index b7d99d2754fa..65825b2444af 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -97,6 +97,8 @@ import com.android.systemui.util.concurrency.FakeExecution; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.google.android.msdl.domain.MSDLPlayer; + import dagger.Lazy; import org.junit.Before; @@ -185,6 +187,8 @@ public class AuthControllerTest extends SysuiTestCase { private Resources mResources; @Mock private VibratorHelper mVibratorHelper; + @Mock + private MSDLPlayer mMSDLPlayer; private TestableContext mContextSpy; private Execution mExecution; @@ -1066,7 +1070,7 @@ public class AuthControllerTest extends SysuiTestCase { () -> mLogContextInteractor, () -> mPromptSelectionInteractor, () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper, - mLazyViewCapture); + mLazyViewCapture, mMSDLPlayer); } @Override diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt index 65236f02b635..e3b5f34c8e5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt @@ -31,6 +31,8 @@ import com.android.systemui.common.ui.data.repository.fakeConfigurationRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.telephony.data.repository.fakeTelephonyRepository @@ -91,6 +93,8 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { kosmos.fakeTelephonyRepository.setHasTelephonyRadio(true) kosmos.telecomManager = telecomManager + + kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "") } @Test @@ -130,6 +134,7 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { assertThat(metricsLogger.logs.element().category) .isEqualTo(MetricsProto.MetricsEvent.ACTION_EMERGENCY_CALL) verify(activityTaskManager).stopSystemLockTaskMode() + assertThat(kosmos.sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen) verify(telecomManager).showInCallScreen(eq(false)) } @@ -156,6 +161,7 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { assertThat(metricsLogger.logs.element().category) .isEqualTo(MetricsProto.MetricsEvent.ACTION_EMERGENCY_CALL) verify(activityTaskManager).stopSystemLockTaskMode() + assertThat(kosmos.sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen) // TODO(b/25189994): Test the activity has been started once we switch to the // ActivityStarter interface here. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java index 53d82d7b2a07..956c12916c98 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineFalsingManagerTest.java @@ -95,6 +95,7 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mFalsingDataProvider.isUnfolded()).thenReturn(false); + when(mFalsingDataProvider.isTouchScreenSource()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassifier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, @@ -193,6 +194,13 @@ public class BrightLineFalsingManagerTest extends SysuiTestCase { } @Test + public void testSkipNonTouchscreenDevices() { + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); + when(mFalsingDataProvider.isTouchScreenSource()).thenReturn(false); + assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isFalse(); + } + + @Test public void testTrackpadGesture() { assertThat(mBrightLineFalsingManager.isFalseTouch(Classifier.GENERIC)).isTrue(); when(mFalsingDataProvider.isFromTrackpad()).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingDataProviderTest.java index 49c6239d2541..df4b0480f5c7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingDataProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/FalsingDataProviderTest.java @@ -18,6 +18,7 @@ package com.android.systemui.classifier; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -25,13 +26,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.devicestate.DeviceStateManager.FoldStateListener; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManagerGlobal; +import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.util.DisplayMetrics; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.classifier.FalsingDataProvider.GestureFinalizedListener; import com.android.systemui.dock.DockManagerFake; import com.android.systemui.statusbar.policy.BatteryController; @@ -56,11 +64,15 @@ public class FalsingDataProviderTest extends ClassifierTest { private FoldStateListener mFoldStateListener; private final DockManagerFake mDockManager = new DockManagerFake(); private DisplayMetrics mDisplayMetrics; + private IInputManager mIInputManager; + private InputManagerGlobal.TestSession inputManagerGlobalTestSession; @Before public void setup() { super.setup(); MockitoAnnotations.initMocks(this); + mIInputManager = mock(IInputManager.Stub.class); + inputManagerGlobalTestSession = InputManagerGlobal.createTestSession(mIInputManager); mDisplayMetrics = new DisplayMetrics(); mDisplayMetrics.xdpi = 100; mDisplayMetrics.ydpi = 100; @@ -73,6 +85,7 @@ public class FalsingDataProviderTest extends ClassifierTest { public void tearDown() { super.tearDown(); mDataProvider.onSessionEnd(); + inputManagerGlobalTestSession.close(); } @Test @@ -378,6 +391,79 @@ public class FalsingDataProviderTest extends ClassifierTest { } @Test + @DisableFlags(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING) + public void test_isTouchscreenSource_flagOff_alwaysTrue() { + assertThat(mDataProvider.isTouchScreenSource()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING) + public void test_isTouchscreenSource_recentEventsEmpty_true() { + //send no events into the data provider + assertThat(mDataProvider.isTouchScreenSource()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING) + public void test_isTouchscreenSource_latestDeviceTouchscreen_true() throws RemoteException { + int deviceId = 999; + + InputDevice device = new InputDevice.Builder() + .setSources(InputDevice.SOURCE_CLASS_TRACKBALL | InputDevice.SOURCE_TOUCHSCREEN) + .setId(deviceId) + .build(); + when(mIInputManager.getInputDeviceIds()).thenReturn(new int[]{deviceId}); + when(mIInputManager.getInputDevice(anyInt())).thenReturn(device); + + MotionEvent event = MotionEvent.obtain(1, 0, MotionEvent.ACTION_UP, 1, + MotionEvent.PointerProperties.createArray(1), + MotionEvent.PointerCoords.createArray(1), 0, 0, 1.0f, 1.0f, deviceId, 0, + InputDevice.SOURCE_CLASS_NONE, 0, 0, 0); + + mDataProvider.onMotionEvent(event); + boolean result = mDataProvider.isTouchScreenSource(); + assertThat(result).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING) + public void test_isTouchscreenSource_latestDeviceNonTouchscreen_false() throws RemoteException { + int deviceId = 9999; + + InputDevice device = new InputDevice.Builder() + .setSources(InputDevice.SOURCE_CLASS_TRACKBALL) + .setId(deviceId) + .build(); + when(mIInputManager.getInputDeviceIds()).thenReturn(new int[]{deviceId}); + when(mIInputManager.getInputDevice(anyInt())).thenReturn(device); + + MotionEvent event = MotionEvent.obtain(1, 0, MotionEvent.ACTION_UP, 1, + MotionEvent.PointerProperties.createArray(1), + MotionEvent.PointerCoords.createArray(1), 0, 0, 1.0f, 1.0f, deviceId, 0, + InputDevice.SOURCE_CLASS_NONE, 0, 0, 0); + + mDataProvider.onMotionEvent(event); + boolean result = mDataProvider.isTouchScreenSource(); + assertThat(result).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_NON_TOUCHSCREEN_DEVICES_BYPASS_FALSING) + public void test_isTouchscreenSource_latestDeviceNull_true() { + // Do not mock InputManager for this test + inputManagerGlobalTestSession.close(); + + int nonExistentDeviceId = 9997; + MotionEvent event = MotionEvent.obtain(1, 0, MotionEvent.ACTION_UP, 1, + MotionEvent.PointerProperties.createArray(1), + MotionEvent.PointerCoords.createArray(1), 0, 0, 1.0f, 1.0f, nonExistentDeviceId, 0, + InputDevice.SOURCE_CLASS_NONE, 0, 0, 0); + + mDataProvider.onMotionEvent(event); + assertThat(mDataProvider.isTouchScreenSource()).isTrue(); + } + + @Test public void test_UnfoldedState_Folded() { FalsingDataProvider falsingDataProvider = createWithFoldCapability(true); when(mFoldStateListener.getFolded()).thenReturn(true); @@ -413,7 +499,7 @@ public class FalsingDataProviderTest extends ClassifierTest { } private FalsingDataProvider createWithFoldCapability(boolean foldable) { - return new FalsingDataProvider( - mDisplayMetrics, mBatteryController, mFoldStateListener, mDockManager, foldable); + return new FalsingDataProvider(mDisplayMetrics, mBatteryController, mFoldStateListener, + mDockManager, foldable); } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt index bb400f274fbe..f06cd6aec8e0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandlerTest.kt @@ -67,7 +67,8 @@ class LongPressHandlingViewInteractionHandlerTest : SysuiTestCase() { isAttachedToWindow = { isAttachedToWindow }, onLongPressDetected = onLongPressDetected, onSingleTapDetected = onSingleTapDetected, - longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() } + longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() }, + allowedTouchSlop = ViewConfiguration.getTouchSlop(), ) underTest.isLongPressHandlingEnabled = true } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt index 3b0057d87048..e531e654cd34 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -73,6 +74,7 @@ class CommunalDreamStartableTest : SysuiTestCase() { keyguardInteractor = kosmos.keyguardInteractor, keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, dreamManager = dreamManager, + communalSceneInteractor = kosmos.communalSceneInteractor, bgScope = kosmos.applicationCoroutineScope, ) .apply { start() } @@ -158,6 +160,36 @@ class CommunalDreamStartableTest : SysuiTestCase() { } } + @Test + fun shouldNotStartDreamWhenLaunchingWidget() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setDreaming(false) + powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON) + kosmos.communalSceneInteractor.setIsLaunchingWidget(true) + whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true) + runCurrent() + + transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB) + + verify(dreamManager, never()).startDream() + } + + @Test + fun shouldNotStartDreamWhenOccluded() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setDreaming(false) + powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON) + keyguardRepository.setKeyguardOccluded(true) + whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true) + runCurrent() + + transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB) + + verify(dreamManager, never()).startDream() + } + private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = from, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt index af76b088787e..af76b088787e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt index c1dcf37498d7..69ccc58cadbf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt @@ -41,7 +41,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.mediaFlags -import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.testKosmos @@ -86,7 +85,6 @@ class MediaDataLoaderTest : SysuiTestCase() { context, testDispatcher, testScope, - kosmos.activityStarter, mediaControllerFactory, mediaFlags, kosmos.imageLoader, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt index 4e5806902a10..5bd3645b4cab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.recordissue.RecordIssueDialogDelegate +import com.android.systemui.screenrecord.RecordingController import com.android.systemui.settings.UserContextProvider import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.phone.KeyguardDismissUtil @@ -40,12 +41,16 @@ 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.Mockito.mock +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class IssueRecordingUserActionInteractorTest : SysuiTestCase() { + @Mock private lateinit var recordingController: RecordingController + val user = UserHandle(1) val kosmos = Kosmos().also { it.testCase = this } @@ -56,6 +61,7 @@ class IssueRecordingUserActionInteractorTest : SysuiTestCase() { @Before fun setup() { + MockitoAnnotations.initMocks(this) hasCreatedDialogDelegate = false with(kosmos) { val factory = @@ -84,7 +90,8 @@ class IssueRecordingUserActionInteractorTest : SysuiTestCase() { dialogTransitionAnimator, panelInteractor, userTracker, - factory + factory, + recordingController, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index 91d8e2a75ef0..de3dc5730421 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -25,7 +25,9 @@ import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R +import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase +import com.android.systemui.SysuiTestableContext import com.android.systemui.common.shared.model.asIcon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -62,7 +64,12 @@ class ModesTileDataInteractorTest : SysuiTestCase() { context.orCreateTestableResources.apply { addOverride(MODES_DRAWABLE_ID, MODES_DRAWABLE) addOverride(R.drawable.ic_zen_mode_type_bedtime, BEDTIME_DRAWABLE) - addOverride(R.drawable.ic_zen_mode_type_driving, DRIVING_DRAWABLE) + } + + val customPackageContext = SysuiTestableContext(context) + context.prepareCreatePackageContext(CUSTOM_PACKAGE, customPackageContext) + customPackageContext.orCreateTestableResources.apply { + addOverride(CUSTOM_DRAWABLE_ID, CUSTOM_DRAWABLE) } } @@ -146,35 +153,41 @@ class ModesTileDataInteractorTest : SysuiTestCase() { assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) - // Add an active mode: icon should be the mode icon. No iconResId, because we don't - // really know that it's a system icon. + // Add an active mode with a default icon: icon should be the mode icon, and the + // iconResId is also populated, because we know it's a system icon. zenModeRepository.addMode( - id = "Bedtime", + id = "Bedtime with default icon", type = AutomaticZenRule.TYPE_BEDTIME, active = true ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isNull() + assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) - // Add another, less-prioritized mode: icon should remain the first mode icon + // Add another, less-prioritized mode that has a *custom* icon: for now, icon should + // remain the first mode icon zenModeRepository.addMode( - id = "Driving", - type = AutomaticZenRule.TYPE_DRIVING, - active = true + TestModeBuilder() + .setId("Driving with custom icon") + .setType(AutomaticZenRule.TYPE_DRIVING) + .setPackage(CUSTOM_PACKAGE) + .setIconResId(CUSTOM_DRAWABLE_ID) + .setActive(true) + .build() ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isNull() + assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) // Deactivate more important mode: icon should be the less important, still active mode - zenModeRepository.deactivateMode("Bedtime") + // And because it's a package-provided icon, iconResId is not populated. + zenModeRepository.deactivateMode("Bedtime with default icon") runCurrent() - assertThat(tileData?.icon).isEqualTo(DRIVING_ICON) + assertThat(tileData?.icon).isEqualTo(CUSTOM_ICON) assertThat(tileData?.iconResId).isNull() // Deactivate remaining mode: back to the default modes icon - zenModeRepository.deactivateMode("Driving") + zenModeRepository.deactivateMode("Driving with custom icon") runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) @@ -241,15 +254,17 @@ class ModesTileDataInteractorTest : SysuiTestCase() { private companion object { val TEST_USER = UserHandle.of(1)!! + const val CUSTOM_PACKAGE = "com.some.mode.owner.package" val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes + const val CUSTOM_DRAWABLE_ID = 12345 val MODES_DRAWABLE = TestStubDrawable("modes_icon") val BEDTIME_DRAWABLE = TestStubDrawable("bedtime") - val DRIVING_DRAWABLE = TestStubDrawable("driving") + val CUSTOM_DRAWABLE = TestStubDrawable("custom") val MODES_ICON = MODES_DRAWABLE.asIcon() val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon() - val DRIVING_ICON = DRIVING_DRAWABLE.asIcon() + val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt index f7bdcb8086ef..c3d45dbbd09a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.tiles.impl.modes.ui import android.app.Flags import android.graphics.drawable.TestStubDrawable -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -109,26 +108,7 @@ class ModesTileMapperTest : SysuiTestCase() { } @Test - @EnableFlags(Flags.FLAG_MODES_UI_ICONS) - fun state_withEnabledFlag_noIconResId() { - val icon = TestStubDrawable("res123").asIcon() - val model = - ModesTileModel( - isActivated = false, - activeModes = emptyList(), - icon = icon, - iconResId = 123 // Should not be populated, but is ignored even if present - ) - - val state = underTest.map(config, model) - - assertThat(state.icon()).isEqualTo(icon) - assertThat(state.iconRes).isNull() - } - - @Test - @DisableFlags(Flags.FLAG_MODES_UI_ICONS) - fun state_withDisabledFlag_includesIconResId() { + fun state_modelHasIconResId_includesIconResId() { val icon = TestStubDrawable("res123").asIcon() val model = ModesTileModel( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index ec79cc6ef5da..d1804608d130 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -21,6 +21,8 @@ package com.android.systemui.scene.domain.startable import android.app.StatusBarManager import android.hardware.face.FaceManager import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,6 +30,8 @@ import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.uiEventLoggerFake import com.android.internal.policy.IKeyguardDismissCallback +import com.android.keyguard.AuthInteractionProperties +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel @@ -48,6 +52,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository @@ -95,6 +100,7 @@ import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvision import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock +import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -137,6 +143,8 @@ class SceneContainerStartableTest : SysuiTestCase() { private val powerInteractor = kosmos.powerInteractor private val fakeTrustRepository = kosmos.fakeTrustRepository private val uiEventLoggerFake = kosmos.uiEventLoggerFake + private val msdlPlayer = kosmos.fakeMSDLPlayer + private val authInteractionProperties = AuthInteractionProperties() private lateinit var underTest: SceneContainerStartable @@ -654,6 +662,7 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessHaptics_onSuccessfulLockscreenAuth_udfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -680,6 +689,31 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_udfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasUdfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessHaptics_onSuccessfulLockscreenAuth_sfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -707,6 +741,32 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps() + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playErrorHaptics_onFailedLockscreenAuth_udfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -727,6 +787,27 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playMSDLErrorHaptics_onFailedLockscreenAuth_udfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasUdfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playErrorHaptics_onFailedLockscreenAuth_sfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -747,6 +828,27 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playMSDLErrorHaptics_onFailedLockscreenAuth_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNotNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsSuccessHaptics_whenPowerButtonDown_sfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -774,6 +876,32 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun skipsMSDLSuccessHaptics_whenPowerButtonDown_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps(isPowerButtonDown = true) + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -801,6 +929,32 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun skipsMSDLSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playSuccessHaptic by + collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + allowHapticsOnSfps(lastPowerPress = 50) + unlockWithFingerprintAuth() + + assertThat(playSuccessHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + + updateFingerprintAuthStatus(isSuccess = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Gone) + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsErrorHaptics_whenPowerButtonDown_sfps() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -822,6 +976,28 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun skipsMSDLErrorHaptics_whenPowerButtonDown_sfps() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasSfps = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + kosmos.fakeKeyEventRepository.setPowerButtonDown(true) + updateFingerprintAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsFaceErrorHaptics_nonSfps_coEx() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -842,6 +1018,26 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic) + + setupBiometricAuth(hasUdfps = true, hasFace = true) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() + + underTest.start() + updateFaceAuthStatus(isSuccess = false) + + assertThat(playErrorHaptic).isNull() + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + assertThat(msdlPlayer.latestTokenPlayed).isNull() + assertThat(msdlPlayer.latestPropertiesPlayed).isNull() + } + + @Test fun hydrateSystemUiState() = testScope.runTest { val transitionStateFlow = prepareState() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 4b132c4276ea..a0bb01797f2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -18,9 +18,12 @@ package com.android.systemui.scene.ui.viewmodel +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.fakeFalsingManager @@ -37,6 +40,10 @@ import com.android.systemui.scene.shared.logger.sceneLogger import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -60,6 +67,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val testScope by lazy { kosmos.testScope } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } + private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } private val falsingManager by lazy { kosmos.fakeFalsingManager } @@ -75,6 +83,8 @@ class SceneContainerViewModelTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, + shadeInteractor = kosmos.shadeInteractor, + splitEdgeDetector = kosmos.splitEdgeDetector, logger = kosmos.sceneLogger, motionEventHandlerReceiver = { motionEventHandler -> this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler @@ -287,4 +297,48 @@ class SceneContainerViewModelTest : SysuiTestCase() { assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade) } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun edgeDetector_singleShade_usesDefaultEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(false) + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + + assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun edgeDetector_splitShade_usesDefaultEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(true) + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + + assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt new file mode 100644 index 000000000000..3d76d280b2cc --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2024 The Android Open 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFailsWith +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SplitEdgeDetectorTest : SysuiTestCase() { + + private val edgeSize = 40 + private val screenWidth = 800 + private val screenHeight = 600 + + private var edgeSplitFraction = 0.7f + + private val underTest = + SplitEdgeDetector( + topEdgeSplitFraction = { edgeSplitFraction }, + edgeSize = edgeSize.dp, + ) + + @Test + fun source_noEdge_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth / 2, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeVerticallyOnTopLeft_detectsTopLeft() { + val detectedEdge = + swipeVerticallyFrom( + x = 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopLeft) + } + + @Test + fun source_swipeHorizontallyOnTopLeft_detectsLeft() { + val detectedEdge = + swipeHorizontallyFrom( + x = 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(Left) + } + + @Test + fun source_swipeVerticallyOnTopRight_detectsTopRight() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopRight) + } + + @Test + fun source_swipeHorizontallyOnTopRight_detectsRight() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(Right) + } + + @Test + fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() { + val detectedEdge = + swipeVerticallyFrom( + x = (screenWidth * edgeSplitFraction).toInt() - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopLeft) + } + + @Test + fun source_swipeVerticallyToRightOfSplit_detectsTopRight() { + val detectedEdge = + swipeVerticallyFrom( + x = (screenWidth * edgeSplitFraction).toInt() + 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopRight) + } + + @Test + fun source_edgeSplitFractionUpdatesDynamically() { + val middleX = (screenWidth * 0.5f).toInt() + val topY = 0 + + // Split closer to the right; middle of screen is considered "left". + edgeSplitFraction = 0.6f + assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft) + + // Split closer to the left; middle of screen is considered "right". + edgeSplitFraction = 0.4f + assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight) + + // Illegal fraction. + edgeSplitFraction = 1.2f + assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } + + // Illegal fraction. + edgeSplitFraction = -0.3f + assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } + } + + @Test + fun source_swipeVerticallyOnBottom_detectsBottom() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth / 3, + y = screenHeight - (edgeSize / 2), + ) + assertThat(detectedEdge).isEqualTo(Bottom) + } + + @Test + fun source_swipeHorizontallyOnBottom_detectsNothing() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth / 3, + y = screenHeight - (edgeSize - 1), + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeHorizontallyOnLeft_detectsLeft() { + val detectedEdge = + swipeHorizontallyFrom( + x = edgeSize - 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isEqualTo(Left) + } + + @Test + fun source_swipeVerticallyOnLeft_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = edgeSize - 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeHorizontallyOnRight_detectsRight() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth - edgeSize + 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isEqualTo(Right) + } + + @Test + fun source_swipeVerticallyOnRight_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth - edgeSize + 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun resolve_startInLtr_resolvesLeft() { + val resolvedEdge = Start.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(Left) + } + + @Test + fun resolve_startInRtl_resolvesRight() { + val resolvedEdge = Start.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(Right) + } + + @Test + fun resolve_endInLtr_resolvesRight() { + val resolvedEdge = End.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(Right) + } + + @Test + fun resolve_endInRtl_resolvesLeft() { + val resolvedEdge = End.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(Left) + } + + @Test + fun resolve_topStartInLtr_resolvesTopLeft() { + val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(TopLeft) + } + + @Test + fun resolve_topStartInRtl_resolvesTopRight() { + val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(TopRight) + } + + @Test + fun resolve_topEndInLtr_resolvesTopRight() { + val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(TopRight) + } + + @Test + fun resolve_topEndInRtl_resolvesTopLeft() { + val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(TopLeft) + } + + private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { + return swipeFrom(x, y, Orientation.Vertical) + } + + private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { + return swipeFrom(x, y, Orientation.Horizontal) + } + + private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? { + return underTest.source( + layoutSize = IntSize(width = screenWidth, height = screenHeight), + position = IntOffset(x, y), + density = Density(1f), + orientation = orientation, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index 3283ea154b3f..d163abf66b05 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -19,12 +19,9 @@ package com.android.systemui.shade.domain.interactor import android.app.StatusBarManager.DISABLE2_NONE import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -39,10 +36,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState -import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.phone.dozeParameters @@ -66,18 +60,17 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { - val kosmos = testKosmos() - val testScope = kosmos.testScope - val configurationRepository by lazy { kosmos.fakeConfigurationRepository } - val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } - val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } - val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } - val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } - val powerRepository by lazy { kosmos.fakePowerRepository } - val shadeTestUtil by lazy { kosmos.shadeTestUtil } - val userRepository by lazy { kosmos.fakeUserRepository } - val userSetupRepository by lazy { kosmos.fakeUserSetupRepository } - val dozeParameters by lazy { kosmos.dozeParameters } + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } + private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val powerRepository by lazy { kosmos.fakePowerRepository } + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } + private val userRepository by lazy { kosmos.fakeUserRepository } + private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository } + private val dozeParameters by lazy { kosmos.dozeParameters } lateinit var underTest: ShadeInteractorImpl @@ -142,9 +135,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { userSetupRepository.setUserSetUp(true) disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NOTIFICATION_SHADE, - ) + DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -158,9 +149,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { userSetupRepository.setUserSetUp(true) disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_QUICK_SETTINGS, - ) + DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS) val actual by collectLastValue(underTest.isExpandToQsEnabled) assertThat(actual).isFalse() @@ -171,10 +160,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) userSetupRepository.setUserSetUp(true) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) keyguardRepository.setIsDozing(true) @@ -188,10 +174,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) @@ -205,10 +188,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = false)) @@ -222,10 +202,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -250,10 +227,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -262,17 +236,12 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { // WHEN QS is disabled disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_QUICK_SETTINGS, - ) + DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS) // THEN expand is disabled assertThat(actual).isFalse() // WHEN QS is enabled - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) // THEN expand is enabled assertThat(actual).isTrue() } @@ -282,10 +251,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -359,9 +325,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { ) ) keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - to = DozeStateModel.DOZE_AOD, - ) + DozeTransitionModel(to = DozeStateModel.DOZE_AOD) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isFalse() @@ -385,9 +349,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { ) ) keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - to = DozeStateModel.DOZE_PULSING, - ) + DozeTransitionModel(to = DozeStateModel.DOZE_PULSING) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isTrue() @@ -450,51 +412,9 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { lastSleepReason = WakeSleepReason.OTHER, ) keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - ) + TransitionStep(transitionState = TransitionState.STARTED) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isTrue() } - - @Test - @DisableFlags(DualShade.FLAG_NAME) - fun legacyShadeMode_narrowScreen_singleShade() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) - - assertThat(shadeMode).isEqualTo(ShadeMode.Single) - } - - @Test - @DisableFlags(DualShade.FLAG_NAME) - fun legacyShadeMode_wideScreen_splitShade() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) - - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - } - - @Test - @EnableFlags(DualShade.FLAG_NAME) - fun shadeMode_wideScreen_isDual() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) - - assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - } - - @Test - @EnableFlags(DualShade.FLAG_NAME) - fun shadeMode_narrowScreen_isDual() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) - - assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt new file mode 100644 index 000000000000..2a2817b9af73 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open 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.shade.domain.interactor + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShadeModeInteractorImplTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: ShadeModeInteractor + + @Before + fun setUp() { + underTest = kosmos.shadeModeInteractor + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_narrowScreen_singleShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_wideScreen_splitShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_wideScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_narrowScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } + + @Test + fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() = + testScope.runTest { + // Ensure isShadeLayoutWide is collected. + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f) + } + + @Test + fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() = + testScope.runTest { + // Ensure isShadeLayoutWide is collected. + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 840aa92548c8..26e1a4d9e961 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -26,6 +26,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.andSceneContainer @@ -36,6 +37,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository @@ -51,6 +53,7 @@ import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -153,7 +156,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenNoNotifs() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -196,7 +199,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -217,7 +220,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenLockedShade() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -315,7 +318,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -333,7 +336,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenLockedShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -351,7 +354,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenKeyguard() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -366,7 +369,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenUserNotSetUp() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -384,7 +387,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenStartingToSleep() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -402,7 +405,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenQsExpandedDefault() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -421,7 +424,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -444,7 +447,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenRemoteInputActive() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -462,7 +465,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_animatesWhenShade() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -478,7 +481,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_notAnimatingOnKeyguard() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -492,6 +495,22 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @EnableSceneContainer + fun shouldShowFooterView_falseWhenShadeIsClosed() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN shade is closed + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + shadeTestUtil.setShadeExpansion(0f) + runCurrent() + + // THEN footer is hidden + assertThat(shouldShow?.value).isFalse() + } + + @Test + @DisableSceneContainer fun shouldHideFooterView_trueWhenShadeIsClosed() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -506,6 +525,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @DisableSceneContainer fun shouldHideFooterView_falseWhenShadeIsOpen() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -520,6 +540,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @DisableSceneContainer fun shouldHideFooterView_falseWhenQSPartiallyOpen() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -642,4 +663,10 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas assertThat(animationsEnabled).isTrue() } + + private fun TestScope.collectFooterViewVisibility() = + collectLastValue( + if (SceneContainerFlag.isEnabled) underTest.shouldShowFooterView + else underTest.shouldIncludeFooterView + ) } diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 141d03599867..e1808fa7532d 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -2002,10 +2002,10 @@ <!-- Shadow for dream overlay status bar complications --> <dimen name="dream_overlay_status_bar_key_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_key_text_shadow_dy">0.5dp</dimen> - <dimen name="dream_overlay_status_bar_key_text_shadow_radius">1dp</dimen> + <dimen name="dream_overlay_status_bar_key_text_shadow_radius">3dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> - <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">3dp</dimen> <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen> <!-- Default device corner radius, used for assist UI --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index dd84bc6989a4..92e5432ad243 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -271,7 +271,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mLatencyTracker, mFalsingCollector, emergencyButtonController, mMessageAreaControllerFactory, - mDevicePostureController, mFeatureFlags, mSelectedUserInteractor); + mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, + mMSDLPlayer); } else if (keyguardInputView instanceof KeyguardPasswordView) { return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index caa74780538e..f74d93e1d88d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -36,6 +36,7 @@ import com.android.internal.widget.LockPatternView.Cell; import com.android.internal.widget.LockscreenCredential; import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; @@ -43,6 +44,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.domain.MSDLPlayer; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -67,6 +70,7 @@ public class KeyguardPatternViewController private LockPatternView mLockPatternView; private CountDownTimer mCountdownTimer; private AsyncTask<?, ?, ?> mPendingLockCheck; + private MSDLPlayer mMSDLPlayer; private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() { @Override @@ -75,6 +79,10 @@ public class KeyguardPatternViewController } }; + private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> { + BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView); + }; + /** * Useful for clearing out the wrong pattern after a delay */ @@ -166,6 +174,10 @@ public class KeyguardPatternViewController boolean isValidPattern) { boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId; if (matched) { + BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback( + /* authenticationSucceeded= */true, + /* player =*/mMSDLPlayer + ); getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0); if (dismissKeyguard) { mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); @@ -173,6 +185,10 @@ public class KeyguardPatternViewController getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern); } } else { + BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback( + /* authenticationSucceeded= */false, + /* player =*/mMSDLPlayer + ); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); if (isValidPattern) { getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs); @@ -200,7 +216,7 @@ public class KeyguardPatternViewController EmergencyButtonController emergencyButtonController, KeyguardMessageAreaController.Factory messageAreaControllerFactory, DevicePostureController postureController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) { super(view, securityMode, keyguardSecurityCallback, emergencyButtonController, messageAreaControllerFactory, featureFlags, selectedUserInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -212,6 +228,7 @@ public class KeyguardPatternViewController featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)); mLockPatternView = mView.findViewById(R.id.lockPatternView); mPostureController = postureController; + mMSDLPlayer = msdlPlayer; } @Override @@ -249,6 +266,7 @@ public class KeyguardPatternViewController if (deadline != 0) { handleAttemptLockout(deadline); } + mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer); } @Override @@ -262,6 +280,7 @@ public class KeyguardPatternViewController cancelBtn.setOnClickListener(null); } mPostureController.removeCallback(mPostureCallback); + mLockPatternView.setExternalHapticsPlayer(null); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java index 2c97d62d690e..4d5e717536f6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java @@ -28,6 +28,7 @@ import android.util.Log; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -68,8 +69,9 @@ public class AccessibilityButtonModeObserver extends } @Inject - public AccessibilityButtonModeObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_MODE); + public AccessibilityButtonModeObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_MODE); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java index 53a21b329594..1363b1c12332 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -49,8 +50,9 @@ public class AccessibilityButtonTargetsObserver extends } @Inject - public AccessibilityButtonTargetsObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); + public AccessibilityButtonTargetsObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java index c94487848b81..736217a699fd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -49,8 +50,9 @@ public class AccessibilityGestureTargetsObserver extends } @Inject - public AccessibilityGestureTargetsObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); + public AccessibilityGestureTargetsObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java index 443441f1ef48..eb4de6837d41 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java @@ -18,6 +18,9 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams; +import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; +import static com.android.systemui.Flags.enableViewCaptureTracing; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -29,8 +32,8 @@ import android.util.MathUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.WindowManager; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.res.R; /** @@ -70,11 +73,12 @@ public abstract class MirrorWindowControl { * @see #setDefaultPosition(LayoutParams) */ private final Point mControlPosition = new Point(); - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; MirrorWindowControl(Context context) { mContext = context; - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, + enableViewCaptureTracing()); } public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java index 326773fb5bef..c50cf85feccb 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.List; @@ -48,6 +49,7 @@ public abstract class SecureSettingsContentObserver<T> { private final UserTracker mUserTracker; @VisibleForTesting final ContentObserver mContentObserver; + private final SecureSettings mSecureSettings; private final String mKey; @@ -55,7 +57,7 @@ public abstract class SecureSettingsContentObserver<T> { final List<T> mListeners = new ArrayList<>(); protected SecureSettingsContentObserver(Context context, UserTracker userTracker, - String secureSettingsKey) { + SecureSettings secureSettings, String secureSettingsKey) { mKey = secureSettingsKey; mContentResolver = context.getContentResolver(); mUserTracker = userTracker; @@ -65,6 +67,7 @@ public abstract class SecureSettingsContentObserver<T> { updateValueChanged(); } }; + mSecureSettings = secureSettings; } /** @@ -80,9 +83,8 @@ public abstract class SecureSettingsContentObserver<T> { } if (mListeners.size() == 1) { - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(mKey), /* notifyForDescendants= */ - false, mContentObserver, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(Settings.Secure.getUriFor(mKey), + /* notifyForDescendants= */ false, mContentObserver, UserHandle.USER_ALL); } } @@ -97,7 +99,7 @@ public abstract class SecureSettingsContentObserver<T> { mListeners.remove(listener); if (mListeners.isEmpty()) { - mContentResolver.unregisterContentObserver(mContentObserver); + mSecureSettings.unregisterContentObserverAsync(mContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 7750f6bf4178..51c5b00daae4 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -18,8 +18,6 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; -import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; - import android.accessibilityservice.AccessibilityService; import android.app.PendingIntent; import android.app.RemoteAction; @@ -45,7 +43,6 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.Flags; import com.android.internal.R; -import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ScreenshotHelper; @@ -561,16 +558,13 @@ public class SystemActions implements CoreStartable, ConfigurationController.Con } private void handleAccessibilityButton() { - AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked( + mA11yManager.notifyAccessibilityButtonClicked( mDisplayTracker.getDefaultDisplayId()); } private void handleAccessibilityButtonChooser() { - final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - final String chooserClassName = AccessibilityButtonChooserActivity.class.getName(); - intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); - mContext.startActivityAsUser(intent, mUserTracker.getUserHandle()); + mA11yManager.notifyAccessibilityButtonLongClicked( + mDisplayTracker.getDefaultDisplayId()); } private void handleAccessibilityShortcut() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt new file mode 100644 index 000000000000..4eb2274cf129 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open 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.data.model + +data class CaptioningModel( + val isSystemAudioCaptioningUiEnabled: Boolean, + val isSystemAudioCaptioningEnabled: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt index bf749d4cfc35..5414b623ff97 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt @@ -16,98 +16,90 @@ package com.android.systemui.accessibility.data.repository +import android.annotation.SuppressLint import android.view.accessibility.CaptioningManager +import com.android.systemui.accessibility.data.model.CaptioningModel +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.utils.UserScopedService +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext interface CaptioningRepository { - /** The system audio caption enabled state. */ - val isSystemAudioCaptioningEnabled: StateFlow<Boolean> + /** Current state of Live Captions. */ + val captioningModel: StateFlow<CaptioningModel?> - /** The system audio caption UI enabled state. */ - val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> - - /** Sets [isSystemAudioCaptioningEnabled]. */ + /** Sets [CaptioningModel.isSystemAudioCaptioningEnabled]. */ suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) } -class CaptioningRepositoryImpl( - private val captioningManager: CaptioningManager, - private val backgroundCoroutineContext: CoroutineContext, - coroutineScope: CoroutineScope, +@OptIn(ExperimentalCoroutinesApi::class) +class CaptioningRepositoryImpl +@Inject +constructor( + private val userScopedCaptioningManagerProvider: UserScopedService<CaptioningManager>, + userRepository: UserRepository, + @Background private val backgroundCoroutineContext: CoroutineContext, + @Application coroutineScope: CoroutineScope, ) : CaptioningRepository { - private val captioningChanges: SharedFlow<CaptioningChange> = - callbackFlow { - val listener = CaptioningChangeProducingListener(this) - captioningManager.addCaptioningChangeListener(listener) - awaitClose { captioningManager.removeCaptioningChangeListener(listener) } - } - .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) - - override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> = - captioningChanges - .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class) - .map { it.isEnabled } - .onStart { emit(captioningManager.isSystemAudioCaptioningEnabled) } - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(), - captioningManager.isSystemAudioCaptioningEnabled, - ) + @SuppressLint("NonInjectedService") // this uses user-aware context + private val captioningManager: StateFlow<CaptioningManager?> = + userRepository.selectedUser + .map { userScopedCaptioningManagerProvider.forUser(it.userInfo.userHandle) } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) - override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> = - captioningChanges - .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class) - .map { it.isEnabled } - .onStart { emit(captioningManager.isSystemAudioCaptioningUiEnabled) } - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(), - captioningManager.isSystemAudioCaptioningUiEnabled, - ) + override val captioningModel: StateFlow<CaptioningModel?> = + captioningManager + .filterNotNull() + .flatMapLatest { it.captioningModel() } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) { withContext(backgroundCoroutineContext) { - captioningManager.isSystemAudioCaptioningEnabled = isEnabled + captioningManager.value?.isSystemAudioCaptioningEnabled = isEnabled } } - private sealed interface CaptioningChange { - - data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange - - data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange - } - - private class CaptioningChangeProducingListener( - private val scope: ProducerScope<CaptioningChange> - ) : CaptioningManager.CaptioningChangeListener() { - - override fun onSystemAudioCaptioningChanged(enabled: Boolean) { - emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled)) - } - - override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) { - emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled)) - } - - private fun emitChange(change: CaptioningChange) { - scope.launch { scope.send(change) } - } + private fun CaptioningManager.captioningModel(): Flow<CaptioningModel> { + return conflatedCallbackFlow { + val listener = + object : CaptioningManager.CaptioningChangeListener() { + + override fun onSystemAudioCaptioningChanged(enabled: Boolean) { + trySend(Unit) + } + + override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) { + trySend(Unit) + } + } + addCaptioningChangeListener(listener) + awaitClose { removeCaptioningChangeListener(listener) } + } + .onStart { emit(Unit) } + .map { + CaptioningModel( + isSystemAudioCaptioningEnabled = isSystemAudioCaptioningEnabled, + isSystemAudioCaptioningUiEnabled = isSystemAudioCaptioningUiEnabled, + ) + } + .flowOn(backgroundCoroutineContext) } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt index 1d493c697652..840edf44ecf5 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt @@ -17,16 +17,22 @@ package com.android.systemui.accessibility.domain.interactor import com.android.systemui.accessibility.data.repository.CaptioningRepository -import kotlinx.coroutines.flow.StateFlow +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map -class CaptioningInteractor(private val repository: CaptioningRepository) { +@SysUISingleton +class CaptioningInteractor @Inject constructor(private val repository: CaptioningRepository) { - val isSystemAudioCaptioningEnabled: StateFlow<Boolean> - get() = repository.isSystemAudioCaptioningEnabled + val isSystemAudioCaptioningEnabled: Flow<Boolean> = + repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningEnabled } - val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> - get() = repository.isSystemAudioCaptioningUiEnabled + val isSystemAudioCaptioningUiEnabled: Flow<Boolean> = + repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningUiEnabled } - suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) = + suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) { repository.setIsSystemAudioCaptioningEnabled(enabled) + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index d08653c3cf1b..60edaae21bc0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -52,7 +52,6 @@ import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HapClientProfile; @@ -105,7 +104,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private final AudioManager mAudioManager; private final LocalBluetoothProfileManager mProfileManager; private final HapClientProfile mHapClientProfile; - private final UiEventLogger mUiEventLogger; + private final HearingDevicesUiEventLogger mUiEventLogger; + private final int mLaunchSourceId; private HearingDevicesListAdapter mDeviceListAdapter; private HearingDevicesPresetsController mPresetsController; private Context mApplicationContext; @@ -153,20 +153,22 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, public interface Factory { /** Create a {@link HearingDevicesDialogDelegate} instance */ HearingDevicesDialogDelegate create( - boolean showPairNewDevice); + boolean showPairNewDevice, + @HearingDevicesUiEventLogger.LaunchSourceId int launchSource); } @AssistedInject public HearingDevicesDialogDelegate( @Application Context applicationContext, @Assisted boolean showPairNewDevice, + @Assisted @HearingDevicesUiEventLogger.LaunchSourceId int launchSourceId, SystemUIDialog.Factory systemUIDialogFactory, ActivityStarter activityStarter, DialogTransitionAnimator dialogTransitionAnimator, @Nullable LocalBluetoothManager localBluetoothManager, @Main Handler handler, AudioManager audioManager, - UiEventLogger uiEventLogger) { + HearingDevicesUiEventLogger uiEventLogger) { mApplicationContext = applicationContext; mShowPairNewDevice = showPairNewDevice; mSystemUIDialogFactory = systemUIDialogFactory; @@ -178,6 +180,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mProfileManager = localBluetoothManager.getProfileManager(); mHapClientProfile = mProfileManager.getHapClientProfile(); mUiEventLogger = uiEventLogger; + mLaunchSourceId = launchSourceId; } @Override @@ -191,7 +194,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, @Override public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId); dismissDialogIfExists(); Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS); Bundle bundle = new Bundle(); @@ -207,15 +210,17 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice(); switch (deviceItem.getType()) { case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT, + mLaunchSourceId); cachedBluetoothDevice.disconnect(); } case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SET_ACTIVE); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SET_ACTIVE, + mLaunchSourceId); cachedBluetoothDevice.setActive(); } case SAVED_BLUETOOTH_DEVICE -> { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_CONNECT); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_CONNECT, mLaunchSourceId); cachedBluetoothDevice.connect(); } } @@ -275,7 +280,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, if (mLocalBluetoothManager == null) { return; } - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId); mPairButton = dialog.requireViewById(R.id.pair_new_device_button); mDeviceList = dialog.requireViewById(R.id.device_list); mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); @@ -363,7 +368,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT, + mLaunchSourceId); mPresetsController.selectPreset( mPresetsController.getAllPresetInfo().get(position).getIndex()); } @@ -381,7 +387,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) { if (visibility == VISIBLE) { mPairButton.setOnClickListener(v -> { - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId); dismissDialogIfExists(); final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -485,7 +491,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, final String name = intent.getComponent() != null ? intent.getComponent().flattenToString() : intent.getPackage() + "/" + intent.getAction(); - mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_RELATED_TOOL_CLICK, 0, name); + mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_RELATED_TOOL_CLICK, + mLaunchSourceId, name); dismissDialogIfExists(); mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, mDialogTransitionAnimator.createActivityTransitionController(view)); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java index bc4cb45582ff..3d24177a8029 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java @@ -70,8 +70,10 @@ public class HearingDevicesDialogManager { * Shows the dialog. * * @param expandable {@link Expandable} from which the dialog is shown. + * @param launchSourceId the id indicates where the dialog is launched from. */ - public void showDialog(Expandable expandable) { + public void showDialog(Expandable expandable, + @HearingDevicesUiEventLogger.LaunchSourceId int launchSourceId) { if (mDialog != null) { if (DEBUG) { Log.d(TAG, "HearingDevicesDialog already showing. Destroy it first."); @@ -91,7 +93,8 @@ public class HearingDevicesDialogManager { }); pairedHearingDeviceCheckTask.addListener(() -> { try { - mDialog = mDialogFactory.create(!pairedHearingDeviceCheckTask.get()).createDialog(); + mDialog = mDialogFactory.create(!pairedHearingDeviceCheckTask.get(), + launchSourceId).createDialog(); if (expandable != null) { DialogTransitionAnimator.Controller controller = diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java index 6a34d1969fe5..02e65fd9031b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility.hearingaid; +import static com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.LAUNCH_SOURCE_A11Y; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -46,7 +48,7 @@ public class HearingDevicesDialogReceiver extends BroadcastReceiver { } if (ACTION.equals(intent.getAction())) { - mDialogManager.showDialog(/* view= */ null); + mDialogManager.showDialog(/* expandable= */ null, LAUNCH_SOURCE_A11Y); } } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java deleted file mode 100644 index 3fbe56eccef2..000000000000 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open 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.hearingaid; - -import com.android.internal.logging.UiEvent; -import com.android.internal.logging.UiEventLogger; - -public enum HearingDevicesUiEvent implements UiEventLogger.UiEventEnum { - - @UiEvent(doc = "Hearing devices dialog is shown") - HEARING_DEVICES_DIALOG_SHOW(1848), - @UiEvent(doc = "Pair new device") - HEARING_DEVICES_PAIR(1849), - @UiEvent(doc = "Connect to the device") - HEARING_DEVICES_CONNECT(1850), - @UiEvent(doc = "Disconnect from the device") - HEARING_DEVICES_DISCONNECT(1851), - @UiEvent(doc = "Set the device as active device") - HEARING_DEVICES_SET_ACTIVE(1852), - @UiEvent(doc = "Click on the device gear to enter device detail page") - HEARING_DEVICES_GEAR_CLICK(1853), - @UiEvent(doc = "Select a preset from preset spinner") - HEARING_DEVICES_PRESET_SELECT(1854), - @UiEvent(doc = "Click on related tool") - HEARING_DEVICES_RELATED_TOOL_CLICK(1856); - - private final int mId; - - HearingDevicesUiEvent(int id) { - mId = id; - } - - @Override - public int getId() { - return mId; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt new file mode 100644 index 000000000000..9e77b02be495 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEvent.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open 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.hearingaid + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger + +enum class HearingDevicesUiEvent(private val id: Int) : UiEventLogger.UiEventEnum { + + @UiEvent(doc = "Hearing devices dialog is shown") HEARING_DEVICES_DIALOG_SHOW(1848), + @UiEvent(doc = "Pair new device") HEARING_DEVICES_PAIR(1849), + @UiEvent(doc = "Connect to the device") HEARING_DEVICES_CONNECT(1850), + @UiEvent(doc = "Disconnect from the device") HEARING_DEVICES_DISCONNECT(1851), + @UiEvent(doc = "Set the device as active device") HEARING_DEVICES_SET_ACTIVE(1852), + @UiEvent(doc = "Click on the device gear to enter device detail page") + HEARING_DEVICES_GEAR_CLICK(1853), + @UiEvent(doc = "Select a preset from preset spinner") HEARING_DEVICES_PRESET_SELECT(1854), + @UiEvent(doc = "Click on related tool") HEARING_DEVICES_RELATED_TOOL_CLICK(1856); + + override fun getId(): Int = this.id +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEventLogger.kt new file mode 100644 index 000000000000..0b32cfc9742b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesUiEventLogger.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open 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.hearingaid + +import android.annotation.IntDef +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class HearingDevicesUiEventLogger @Inject constructor(private val uiEventLogger: UiEventLogger) { + + /** Logs the given event */ + fun log(event: UiEventLogger.UiEventEnum, launchSourceId: Int) { + log(event, launchSourceId, null) + } + + fun log(event: UiEventLogger.UiEventEnum, launchSourceId: Int, pkgName: String?) { + uiEventLogger.log(event, launchSourceId, pkgName) + } + + /** + * The possible launch source of hearing devices dialog + * + * @hide + */ + @IntDef(LAUNCH_SOURCE_UNKNOWN, LAUNCH_SOURCE_A11Y, LAUNCH_SOURCE_QS_TILE) + annotation class LaunchSourceId + + companion object { + const val LAUNCH_SOURCE_UNKNOWN = 0 + const val LAUNCH_SOURCE_A11Y = 1 // launch from AccessibilityManagerService + const val LAUNCH_SOURCE_QS_TILE = 2 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 970fdeabcaf8..69ab976c5301 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -78,6 +78,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.google.android.msdl.domain.MSDLPlayer; + import kotlin.Lazy; import kotlinx.coroutines.CoroutineScope; @@ -157,6 +159,8 @@ public class AuthContainerView extends LinearLayout private final @Background DelayableExecutor mBackgroundExecutor; + private final MSDLPlayer mMSDLPlayer; + // Non-null only if the dialog is in the act of dismissing and has not sent the reason yet. @Nullable @AuthDialogCallback.DismissedReason private Integer mPendingCallbackReason; // HAT received from LockSettingsService when credential is verified. @@ -292,7 +296,8 @@ public class AuthContainerView extends LinearLayout @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull @Background DelayableExecutor bgExecutor, @NonNull VibratorHelper vibratorHelper, - Lazy<ViewCapture> lazyViewCapture) { + Lazy<ViewCapture> lazyViewCapture, + @NonNull MSDLPlayer msdlPlayer) { super(config.mContext); mConfig = config; @@ -309,6 +314,7 @@ public class AuthContainerView extends LinearLayout .getDimension(R.dimen.biometric_dialog_animation_translation_offset); mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); + mMSDLPlayer = msdlPlayer; final BiometricModalities biometricModalities = new BiometricModalities( Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds), @@ -379,7 +385,7 @@ public class AuthContainerView extends LinearLayout getJankListener(mLayout, TRANSIT, BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS), mBackgroundView, mBiometricCallback, mApplicationCoroutineScope, - vibratorHelper); + vibratorHelper, mMSDLPlayer); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 097ab72522d6..b39aae94ed4e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -89,6 +89,8 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.Execution; +import com.google.android.msdl.domain.MSDLPlayer; + import dagger.Lazy; import kotlin.Unit; @@ -183,6 +185,7 @@ public class AuthController implements private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); @NonNull private final VibratorHelper mVibratorHelper; + @NonNull private final MSDLPlayer mMSDLPlayer; private final kotlin.Lazy<ViewCapture> mLazyViewCapture; @@ -742,7 +745,8 @@ public class AuthController implements @Background DelayableExecutor bgExecutor, @NonNull UdfpsUtils udfpsUtils, @NonNull VibratorHelper vibratorHelper, - Lazy<ViewCapture> daggerLazyViewCapture) { + Lazy<ViewCapture> daggerLazyViewCapture, + @NonNull MSDLPlayer msdlPlayer) { mContext = context; mExecution = execution; mUserManager = userManager; @@ -764,6 +768,7 @@ public class AuthController implements mUdfpsUtils = udfpsUtils; mApplicationCoroutineScope = applicationCoroutineScope; mVibratorHelper = vibratorHelper; + mMSDLPlayer = msdlPlayer; mLogContextInteractor = logContextInteractor; mPromptSelectorInteractor = promptSelectorInteractorProvider; @@ -1327,7 +1332,7 @@ public class AuthController implements wakefulnessLifecycle, userManager, lockPatternUtils, mInteractionJankMonitor, mPromptSelectorInteractor, viewModel, mCredentialViewModelProvider, bgExecutor, mVibratorHelper, - mLazyViewCapture); + mLazyViewCapture, mMSDLPlayer); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 0b440ad81fb5..e7e8d8f80cda 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -25,7 +25,6 @@ import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.Flags import android.hardware.face.FaceManager import android.util.Log -import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO @@ -59,6 +58,7 @@ import com.android.systemui.common.ui.view.onTouchListener import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper +import com.google.android.msdl.domain.MSDLPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine @@ -83,6 +83,7 @@ object BiometricViewBinder { legacyCallback: Spaghetti.Callback, applicationScope: CoroutineScope, vibratorHelper: VibratorHelper, + msdlPlayer: MSDLPlayer, ): Spaghetti { val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!! @@ -434,21 +435,27 @@ object BiometricViewBinder { // Play haptics launch { viewModel.hapticsToPlay.collect { haptics -> - if (haptics.hapticFeedbackConstant != HapticFeedbackConstants.NO_HAPTICS) { - if (haptics.flag != null) { - vibratorHelper.performHapticFeedback( - view, - haptics.hapticFeedbackConstant, - haptics.flag, - ) - } else { - vibratorHelper.performHapticFeedback( - view, - haptics.hapticFeedbackConstant, - ) + when (haptics) { + is PromptViewModel.HapticsToPlay.HapticConstant -> { + if (haptics.flag != null) { + vibratorHelper.performHapticFeedback( + view, + haptics.constant, + haptics.flag, + ) + } else { + vibratorHelper.performHapticFeedback( + view, + haptics.constant, + ) + } + } + is PromptViewModel.HapticsToPlay.MSDL -> { + msdlPlayer.playToken(haptics.token, haptics.properties) } - viewModel.clearHaptics() + is PromptViewModel.HapticsToPlay.None -> {} } + viewModel.clearHaptics() } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 85c3ae3f214e..d69e87534cb6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -18,13 +18,11 @@ package com.android.systemui.biometrics.ui.binder import android.animation.Animator import android.animation.AnimatorSet -import android.animation.ValueAnimator import android.graphics.Outline import android.graphics.Rect import android.transition.AutoTransition import android.transition.TransitionManager import android.util.TypedValue -import android.view.Surface import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider @@ -413,13 +411,12 @@ object BiometricViewSizeBinder { ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() ) - TransitionManager.beginDelayedTransition(view, autoTransition) - if (position.isLeft) { flipConstraintSet.applyTo(view) } else { mediumConstraintSet.applyTo(view) } + TransitionManager.beginDelayedTransition(view, autoTransition) } size.isMedium -> { if (position.isLeft) { @@ -428,14 +425,18 @@ object BiometricViewSizeBinder { mediumConstraintSet.applyTo(view) } } - size.isLarge && currentSize.isMedium -> { + size.isLarge -> { val autoTransition = AutoTransition() autoTransition.setDuration( - ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() + if (currentSize.isSmall) { + ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong() + } else { + ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong() + } ) - TransitionManager.beginDelayedTransition(view, autoTransition) largeConstraintSet.applyTo(view) + TransitionManager.beginDelayedTransition(view, autoTransition) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 4c2fe07f92bb..168ba11309cc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -35,7 +35,9 @@ import android.util.Log import android.util.RotationUtils import android.view.HapticFeedbackConstants import android.view.MotionEvent +import com.android.keyguard.AuthInteractionProperties import com.android.launcher3.icons.IconProvider +import com.android.systemui.Flags.msdlFeedback import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.Utils.isSystem @@ -53,6 +55,8 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.res.R import com.android.systemui.util.kotlin.combine +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.InteractionProperties import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope @@ -245,8 +249,9 @@ constructor( private val _forceLargeSize = MutableStateFlow(false) private val _forceMediumSize = MutableStateFlow(false) - private val _hapticsToPlay = - MutableStateFlow(HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, /* flag= */ null)) + private val authInteractionProperties = AuthInteractionProperties() + private val _hapticsToPlay: MutableStateFlow<HapticsToPlay> = + MutableStateFlow(HapticsToPlay.None) /** Event fired to the view indicating a [HapticsToPlay] */ val hapticsToPlay = _hapticsToPlay.asStateFlow() @@ -939,26 +944,52 @@ constructor( } private fun vibrateOnSuccess() { - _hapticsToPlay.value = - HapticsToPlay( - HapticFeedbackConstants.BIOMETRIC_CONFIRM, - null, - ) + val haptics = + if (msdlFeedback()) { + HapticsToPlay.MSDL(MSDLToken.UNLOCK, authInteractionProperties) + } else { + HapticsToPlay.HapticConstant( + HapticFeedbackConstants.BIOMETRIC_CONFIRM, + flag = null, + ) + } + _hapticsToPlay.value = haptics } private fun vibrateOnError() { - _hapticsToPlay.value = - HapticsToPlay( - HapticFeedbackConstants.BIOMETRIC_REJECT, - null, - ) + val haptics = + if (msdlFeedback()) { + HapticsToPlay.MSDL(MSDLToken.FAILURE, authInteractionProperties) + } else { + HapticsToPlay.HapticConstant( + HapticFeedbackConstants.BIOMETRIC_REJECT, + flag = null, + ) + } + _hapticsToPlay.value = haptics } /** Clears the [hapticsToPlay] variable by setting its constant to the NO_HAPTICS default. */ fun clearHaptics() { - _hapticsToPlay.update { previous -> - HapticsToPlay(HapticFeedbackConstants.NO_HAPTICS, previous.flag) - } + _hapticsToPlay.update { HapticsToPlay.None } + } + + /** The state of haptic feedback to play. */ + sealed interface HapticsToPlay { + /** + * Haptics using [HapticFeedbackConstants]. It is composed by a [HapticFeedbackConstants] + * and a [HapticFeedbackConstants] flag. + */ + data class HapticConstant(val constant: Int, val flag: Int?) : HapticsToPlay + + /** + * Haptics using MSDL feedback. It is composed by a [MSDLToken] and optional + * [InteractionProperties] + */ + data class MSDL(val token: MSDLToken, val properties: InteractionProperties?) : + HapticsToPlay + + data object None : HapticsToPlay } companion object { @@ -1095,9 +1126,3 @@ enum class FingerprintStartMode { val isStarted: Boolean get() = this == Normal || this == Delayed } - -/** - * The state of haptic feedback to play. It is composed by a [HapticFeedbackConstants] and a - * [HapticFeedbackConstants] flag. - */ -data class HapticsToPlay(val hapticFeedbackConstant: Int, val flag: Int?) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt index f36ef6630a48..8b5a09b3d9fd 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt @@ -34,10 +34,14 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.doze.DozeLogger +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.EmergencyDialerConstants +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -69,6 +73,7 @@ constructor( private val emergencyDialerIntentFactory: EmergencyDialerIntentFactory, private val metricsLogger: MetricsLogger, private val dozeLogger: DozeLogger, + private val sceneInteractor: Lazy<SceneInteractor>, ) { /** The bouncer action button. If `null`, the button should not be shown. */ val actionButton: Flow<BouncerActionButtonModel?> = @@ -158,14 +163,17 @@ constructor( } private fun prepareToPerformAction() { - // TODO(b/308001302): Trigger occlusion and resetting bouncer state. + if (SceneContainerFlag.isEnabled) { + sceneInteractor.get().changeScene(Scenes.Lockscreen, "Bouncer action button clicked") + } + metricsLogger.action(MetricsEvent.ACTION_EMERGENCY_CALL) activityTaskManager.stopSystemLockTaskMode() } @SuppressLint("MissingPermission") private fun returnToCall() { - telecomManager?.showInCallScreen(/* showDialpad = */ false) + telecomManager?.showInCallScreen(/* showDialpad= */ false) } private val <T> Flow<T>.asUnitFlow: Flow<Unit> diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt new file mode 100644 index 000000000000..1faacff996ca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open 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.bouncer.ui.helper + +import android.view.HapticFeedbackConstants +import android.view.View +import com.android.keyguard.AuthInteractionProperties +import com.android.systemui.Flags +//noinspection CleanArchitectureDependencyViolation: Data layer only referenced for this enum class +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer + +/** A helper object to deliver haptic feedback in bouncer interactions. */ +object BouncerHapticHelper { + + private val authInteractionProperties = AuthInteractionProperties() + + /** + * Deliver MSDL feedback as a result of authenticating through a bouncer. + * + * @param[authenticationSucceeded] Whether the authentication was successful or not. + * @param[player] The [MSDLPlayer] that delivers the correct feedback. + */ + fun playMSDLAuthenticationFeedback( + authenticationSucceeded: Boolean, + player: MSDLPlayer?, + ) { + if (player == null || !Flags.msdlFeedback()) { + return + } + + val token = + if (authenticationSucceeded) { + MSDLToken.UNLOCK + } else { + MSDLToken.FAILURE + } + player.playToken(token, authInteractionProperties) + } + + /** + * Deliver feedback when dragging through cells in the pattern bouncer. This function can play + * MSDL feedback using a [MSDLPlayer], or fallback to a default haptic feedback using the + * [View.performHapticFeedback] API and a [View]. + * + * @param[player] [MSDLPlayer] for MSDL feedback. + * @param[view] A [View] for default haptic feedback using [View.performHapticFeedback] + */ + fun playPatternDotFeedback(player: MSDLPlayer?, view: View?) { + if (player == null || !Flags.msdlFeedback()) { + view?.performHapticFeedback( + HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING, + ) + } else { + player.playToken(MSDLToken.DRAG_INDICATOR) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java index e1ba93c75aad..83d4091802d7 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java @@ -34,8 +34,6 @@ import com.android.internal.logging.MetricsLogger; import com.android.systemui.classifier.FalsingDataProvider.SessionListener; import com.android.systemui.classifier.HistoryTracker.BeliefListener; import com.android.systemui.dagger.qualifiers.TestHarness; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -396,6 +394,7 @@ public class BrightLineFalsingManager implements FalsingManager { || mDataProvider.isA11yAction() || mDataProvider.isFromTrackpad() || mDataProvider.isFromKeyboard() + || !mDataProvider.isTouchScreenSource() || mDataProvider.isUnfolded(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index 2eca02c2b0d1..962ab998ddb1 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -21,6 +21,7 @@ import static com.android.systemui.dock.DockManager.DockEventListener; import android.hardware.SensorManager; import android.hardware.biometrics.BiometricSourceType; import android.util.Log; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; @@ -28,6 +29,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.Flags; import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; @@ -343,7 +345,9 @@ class FalsingCollectorImpl implements FalsingCollector { // will be ignored by the collector until another MotionEvent.ACTION_DOWN is passed in. // avoidGesture must be called immediately following the MotionEvent.ACTION_DOWN, before // any other events are processed, otherwise the whole gesture will be recorded. - if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + // + // We should only delay processing of these events for touchscreen sources + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN && isTouchscreenSource(ev)) { // Make a copy of ev, since it will be recycled after we exit this method. mPendingDownEvent = MotionEvent.obtain(ev); mAvoidGesture = false; @@ -410,6 +414,22 @@ class FalsingCollectorImpl implements FalsingCollector { mFalsingDataProvider.onA11yAction(); } + /** + * returns {@code true} if the device supports Touchscreen, {@code false} otherwise. Defaults to + * {@code true} if the device is {@code null} + */ + private boolean isTouchscreenSource(MotionEvent ev) { + if (!Flags.nonTouchscreenDevicesBypassFalsing()) { + return true; + } + InputDevice device = ev.getDevice(); + if (device != null) { + return device.supportsSource(InputDevice.SOURCE_TOUCHSCREEN); + } else { + return true; + } + } + private boolean shouldSessionBeActive() { return mScreenOn && (mState == StatusBarState.KEYGUARD) diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java index 15017011134b..769976ef5058 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java @@ -20,11 +20,13 @@ import static com.android.systemui.classifier.FalsingModule.IS_FOLDABLE_DEVICE; import android.hardware.devicestate.DeviceStateManager.FoldStateListener; import android.util.DisplayMetrics; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.statusbar.policy.BatteryController; @@ -281,6 +283,9 @@ public class FalsingDataProvider { } public boolean isFromTrackpad() { + if (Flags.nonTouchscreenDevicesBypassFalsing()) { + return false; + } if (mRecentMotionEvents.isEmpty()) { return false; } @@ -290,6 +295,25 @@ public class FalsingDataProvider { || classification == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE; } + /** + * returns {@code true} if the device supports Touchscreen, {@code false} otherwise. Defaults to + * {@code true} if the device is {@code null} + */ + public boolean isTouchScreenSource() { + if (!Flags.nonTouchscreenDevicesBypassFalsing()) { + return true; + } + if (mRecentMotionEvents.isEmpty()) { + return true; + } + InputDevice device = mRecentMotionEvents.get(mRecentMotionEvents.size() - 1).getDevice(); + if (device != null) { + return device.supportsSource(InputDevice.SOURCE_TOUCHSCREEN); + } else { + return true; + } + } + private void recalculateData() { if (!mDirty) { return; diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index b6ace81d18ba..9c4736a13b46 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -27,6 +27,7 @@ import android.view.ViewConfiguration import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.android.systemui.log.LongPressHandlingViewLogger import com.android.systemui.shade.TouchLogger import kotlin.math.pow import kotlin.math.sqrt @@ -42,6 +43,8 @@ class LongPressHandlingView( context: Context, attrs: AttributeSet?, longPressDuration: () -> Long, + allowedTouchSlop: Int = ViewConfiguration.getTouchSlop(), + logger: LongPressHandlingViewLogger? = null, ) : View( context, @@ -97,6 +100,8 @@ class LongPressHandlingView( }, onSingleTapDetected = { listener?.onSingleTapDetected(this@LongPressHandlingView) }, longPressDuration = longPressDuration, + allowedTouchSlop = allowedTouchSlop, + logger = logger, ) } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt index d3fc610bc52e..4e38a4913fe6 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt @@ -17,7 +17,7 @@ package com.android.systemui.common.ui.view -import android.view.ViewConfiguration +import com.android.systemui.log.LongPressHandlingViewLogger import kotlinx.coroutines.DisposableHandle /** Encapsulates logic to handle complex touch interactions with a [LongPressHandlingView]. */ @@ -35,6 +35,14 @@ class LongPressHandlingViewInteractionHandler( private val onSingleTapDetected: () -> Unit, /** Time for the touch to be considered a long-press in ms */ var longPressDuration: () -> Long, + /** + * Default touch slop that is allowed, if the movement between [MotionEventModel.Down] and + * [MotionEventModel.Up] is more than [allowedTouchSlop] then the touch is not processed as + * single tap or a long press. + */ + val allowedTouchSlop: Int, + /** Optional logger that can be passed in to log touch events */ + val logger: LongPressHandlingViewLogger? = null, ) { sealed class MotionEventModel { object Other : MotionEventModel() @@ -70,22 +78,26 @@ class LongPressHandlingViewInteractionHandler( true } is MotionEventModel.Move -> { - if (event.distanceMoved > ViewConfiguration.getTouchSlop()) { + if (event.distanceMoved > allowedTouchSlop) { + logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop) cancelScheduledLongPress() } false } is MotionEventModel.Up -> { + logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration) cancelScheduledLongPress() if ( - event.distanceMoved <= ViewConfiguration.getTouchSlop() && + event.distanceMoved <= allowedTouchSlop && event.gestureDuration < longPressDuration() ) { + logger?.dispatchingSingleTap() dispatchSingleTap() } false } is MotionEventModel.Cancel -> { + logger?.motionEventCancelled() cancelScheduledLongPress() false } @@ -97,15 +109,18 @@ class LongPressHandlingViewInteractionHandler( x: Int, y: Int, ) { + val duration = longPressDuration() + logger?.schedulingLongPress(duration) scheduledLongPressHandle = postDelayed( { + logger?.longPressTriggered() dispatchLongPress( x = x, y = y, ) }, - longPressDuration(), + duration, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt index c69cea4a6a5a..04393feaae37 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt @@ -21,6 +21,7 @@ import android.app.DreamManager import com.android.systemui.CoreStartable import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming import com.android.systemui.Flags.restartDreamOnUnocclude +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -55,6 +56,7 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val dreamManager: DreamManager, + private val communalSceneInteractor: CommunalSceneInteractor, @Background private val bgScope: CoroutineScope, ) : CoreStartable { /** Flow that emits when the dream should be started underneath the glanceable hub. */ @@ -66,6 +68,8 @@ constructor( not(keyguardInteractor.isDreaming), // TODO(b/362830856): Remove this workaround. keyguardInteractor.isKeyguardShowing, + not(communalSceneInteractor.isLaunchingWidget), + not(keyguardInteractor.isKeyguardOccluded), ) .filter { it } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 21a704df074e..8818c3af4916 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -202,6 +202,13 @@ public class FrameworkServicesModule { return context.getSystemService(CaptioningManager.class); } + @Provides + @Singleton + static UserScopedService<CaptioningManager> provideUserScopedCaptioningManager( + Context context) { + return new UserScopedServiceImpl<>(context, CaptioningManager.class); + } + /** */ @Provides @Singleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 1bc91cac1a84..67625d04b467 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -81,6 +81,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardStateCallbackInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardWakeDirectlyToGoneInteractor; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder; @@ -126,6 +127,7 @@ public class KeyguardService extends Service { private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy; private final Executor mMainExecutor; private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy; + private final KeyguardStateCallbackInteractor mKeyguardStateCallbackInteractor; private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers, SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap, @@ -350,7 +352,8 @@ public class KeyguardService extends Service { Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy, KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor, KeyguardDismissInteractor keyguardDismissInteractor, - Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy) { + Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy, + KeyguardStateCallbackInteractor keyguardStateCallbackInteractor) { super(); mKeyguardViewMediator = keyguardViewMediator; mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher; @@ -363,6 +366,7 @@ public class KeyguardService extends Service { mSceneInteractorLazy = sceneInteractorLazy; mMainExecutor = mainExecutor; mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy; + mKeyguardStateCallbackInteractor = keyguardStateCallbackInteractor; mDeviceEntryInteractorLazy = deviceEntryInteractorLazy; if (KeyguardWmStateRefactor.isEnabled()) { @@ -455,6 +459,8 @@ public class KeyguardService extends Service { checkPermission(); if (SceneContainerFlag.isEnabled()) { mKeyguardStateCallbackStartableLazy.get().addCallback(callback); + } else if (KeyguardWmStateRefactor.isEnabled()) { + mKeyguardStateCallbackInteractor.addCallback(callback); } else { mKeyguardViewMediator.addStateMonitorCallback(callback); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index d38c9520eb7a..3b1569d7f79b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2288,6 +2288,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private void updateInputRestrictedLocked() { + if (KeyguardWmStateRefactor.isEnabled()) { + return; + } + boolean inputRestricted = isInputRestricted(); if (mInputRestricted != inputRestricted) { mInputRestricted = inputRestricted; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index db5a63bbf446..58c8a0456241 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -73,7 +73,7 @@ constructor( if (SceneContainerFlag.isEnabled) return listenForGoneToAodOrDozing() listenForGoneToDreaming() - listenForGoneToLockscreenOrHub() + listenForGoneToLockscreenOrHubOrOccluded() listenForGoneToOccluded() listenForGoneToDreamingLockscreenHosted() } @@ -89,22 +89,19 @@ constructor( */ private fun listenForGoneToOccluded() { scope.launch("$TAG#listenForGoneToOccluded") { - keyguardInteractor.showDismissibleKeyguard - .filterRelevantKeyguardState() - .sample(keyguardInteractor.isKeyguardOccluded, ::Pair) - .collect { (_, isKeyguardOccluded) -> - if (isKeyguardOccluded) { - startTransitionTo( - KeyguardState.OCCLUDED, - ownerReason = "Dismissible keyguard with occlusion" - ) - } + keyguardInteractor.showDismissibleKeyguard.filterRelevantKeyguardState().collect { + if (keyguardInteractor.isKeyguardOccluded.value) { + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "Dismissible keyguard with occlusion" + ) } + } } } // Primarily for when the user chooses to lock down the device - private fun listenForGoneToLockscreenOrHub() { + private fun listenForGoneToLockscreenOrHubOrOccluded() { if (KeyguardWmStateRefactor.isEnabled) { scope.launch("$TAG#listenForGoneToLockscreenOrHub") { biometricSettingsRepository.isCurrentUserInLockdown @@ -137,7 +134,7 @@ constructor( } } } else { - scope.launch("$TAG#listenForGoneToLockscreenOrHub") { + scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") { keyguardInteractor.isKeyguardShowing .filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing } .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair) @@ -145,6 +142,8 @@ constructor( val to = if (isIdleOnCommunal) { KeyguardState.GLANCEABLE_HUB + } else if (keyguardInteractor.isKeyguardOccluded.value) { + KeyguardState.OCCLUDED } else { KeyguardState.LOCKSCREEN } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index e2bb540f6645..7afc7596a994 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -80,10 +80,7 @@ constructor( } applicationScope.launch { val refreshConfig = - Config( - Type.NoTransition, - rebuildSections = listOf(smartspaceSection), - ) + Config(Type.NoTransition, rebuildSections = listOf(smartspaceSection)) configurationInteractor.onAnyConfigurationChange.collect { refreshBlueprint(refreshConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt new file mode 100644 index 000000000000..420fbd4ae48d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open 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.domain.interactor + +import android.os.DeadObjectException +import android.os.RemoteException +import com.android.internal.policy.IKeyguardStateCallback +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.KeyguardWmStateRefactor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * Updates KeyguardStateCallbacks provided to KeyguardService with KeyguardTransitionInteractor + * state. + * + * This borrows heavily from [KeyguardStateCallbackStartable], which requires Flexiglass. This class + * can be removed after Flexiglass launches. + */ +@SysUISingleton +class KeyguardStateCallbackInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val selectedUserInteractor: SelectedUserInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : CoreStartable { + private val callbacks = mutableListOf<IKeyguardStateCallback>() + + override fun start() { + if (!KeyguardWmStateRefactor.isEnabled || SceneContainerFlag.isEnabled) { + return + } + + applicationScope.launch { + combine( + selectedUserInteractor.selectedUser, + keyguardTransitionInteractor.currentKeyguardState, + ::Pair + ).collectLatest { (selectedUser, currentState) -> + val iterator = callbacks.iterator() + withContext(backgroundDispatcher) { + while (iterator.hasNext()) { + val callback = iterator.next() + try { + callback.onShowingStateChanged( + currentState != KeyguardState.GONE, + selectedUser + ) + callback.onInputRestrictedStateChanged( + currentState != KeyguardState.GONE) + } catch (e: RemoteException) { + if (e is DeadObjectException) { + iterator.remove() + } + } + } + } + } + } + } + + fun addCallback(callback: IKeyguardStateCallback) { + KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode() + callbacks.add(callback) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index d9c48fa7e581..25b8fd32e82a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -29,6 +29,7 @@ constructor( private val auditLogger: KeyguardTransitionAuditLogger, private val bootInteractor: KeyguardTransitionBootInteractor, private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor, + private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor, ) : CoreStartable { override fun start() { @@ -55,6 +56,7 @@ constructor( auditLogger.start() bootInteractor.start() statusBarDisableFlagsInteractor.start() + keyguardStateCallbackInteractor.start() } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt index e00e33df62eb..43aab355d6d1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StatusBarDisableFlagsInteractor.kt @@ -39,12 +39,14 @@ import com.android.systemui.navigation.domain.interactor.NavigationInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessModel +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.user.domain.interactor.SelectedUserInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -75,62 +77,66 @@ constructor( private val disableToken: IBinder = Binder() private val disableFlagsForUserId = - combine( - selectedUserInteractor.selectedUser, - keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to }, - deviceConfigInteractor.property( - namespace = DeviceConfig.NAMESPACE_SYSTEMUI, - name = SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN, - default = true, - ), - navigationInteractor.isGesturalMode, - authenticationInteractor.authenticationMethod, - powerInteractor.detailedWakefulness, - ) { values -> - val selectedUserId = values[0] as Int - val startedState = values[1] as KeyguardState - val isShowHomeOverLockscreen = values[2] as Boolean - val isGesturalMode = values[3] as Boolean - val authenticationMethod = values[4] as AuthenticationMethodModel - val wakefulnessModel = values[5] as WakefulnessModel - val isOccluded = startedState == KeyguardState.OCCLUDED + if (!KeyguardWmStateRefactor.isEnabled || SceneContainerFlag.isEnabled) { + flowOf(Pair(0, StatusBarManager.DISABLE_NONE)) + } else { + combine( + selectedUserInteractor.selectedUser, + keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to }, + deviceConfigInteractor.property( + namespace = DeviceConfig.NAMESPACE_SYSTEMUI, + name = SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN, + default = true, + ), + navigationInteractor.isGesturalMode, + authenticationInteractor.authenticationMethod, + powerInteractor.detailedWakefulness, + ) { values -> + val selectedUserId = values[0] as Int + val startedState = values[1] as KeyguardState + val isShowHomeOverLockscreen = values[2] as Boolean + val isGesturalMode = values[3] as Boolean + val authenticationMethod = values[4] as AuthenticationMethodModel + val wakefulnessModel = values[5] as WakefulnessModel + val isOccluded = startedState == KeyguardState.OCCLUDED - val hideHomeAndRecentsForBouncer = - startedState == KeyguardState.PRIMARY_BOUNCER || - startedState == KeyguardState.ALTERNATE_BOUNCER - val isKeyguardShowing = startedState != KeyguardState.GONE - val isPowerGestureIntercepted = - with(wakefulnessModel) { - isAwake() && - powerButtonLaunchGestureTriggered && - lastSleepReason == WakeSleepReason.POWER_BUTTON - } + val hideHomeAndRecentsForBouncer = + startedState == KeyguardState.PRIMARY_BOUNCER || + startedState == KeyguardState.ALTERNATE_BOUNCER + val isKeyguardShowing = startedState != KeyguardState.GONE + val isPowerGestureIntercepted = + with(wakefulnessModel) { + isAwake() && + powerButtonLaunchGestureTriggered && + lastSleepReason == WakeSleepReason.POWER_BUTTON + } - var flags = StatusBarManager.DISABLE_NONE + var flags = StatusBarManager.DISABLE_NONE - if (hideHomeAndRecentsForBouncer || (isKeyguardShowing && !isOccluded)) { - if (!isShowHomeOverLockscreen || !isGesturalMode) { - flags = flags or StatusBarManager.DISABLE_HOME + if (hideHomeAndRecentsForBouncer || (isKeyguardShowing && !isOccluded)) { + if (!isShowHomeOverLockscreen || !isGesturalMode) { + flags = flags or StatusBarManager.DISABLE_HOME + } + flags = flags or StatusBarManager.DISABLE_RECENT } - flags = flags or StatusBarManager.DISABLE_RECENT - } - if ( - isPowerGestureIntercepted && - isOccluded && - authenticationMethod.isSecure && - deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() - ) { - flags = flags or StatusBarManager.DISABLE_RECENT - } + if ( + isPowerGestureIntercepted && + isOccluded && + authenticationMethod.isSecure && + deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() + ) { + flags = flags or StatusBarManager.DISABLE_RECENT + } - selectedUserId to flags - } - .distinctUntilChanged() + selectedUserId to flags + } + .distinctUntilChanged() + } @SuppressLint("WrongConstant", "NonInjectedService") override fun start() { - if (!KeyguardWmStateRefactor.isEnabled) { + if (!KeyguardWmStateRefactor.isEnabled || SceneContainerFlag.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index 91a7f7fc66bd..76962732ad01 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.log.LongPressHandlingViewLogger import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scrim.ScrimView @@ -191,6 +192,7 @@ constructor( optionallyAddUdfpsViews( view = view, + logger = alternateBouncerDependencies.logger, udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel, udfpsA11yOverlayViewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel, @@ -248,6 +250,7 @@ constructor( private fun optionallyAddUdfpsViews( view: ConstraintLayout, + logger: LongPressHandlingViewLogger, udfpsIconViewModel: AlternateBouncerUdfpsIconViewModel, udfpsA11yOverlayViewModel: Lazy<AlternateBouncerUdfpsAccessibilityOverlayViewModel>, ) { @@ -276,7 +279,7 @@ constructor( var udfpsView = view.getViewById(udfpsViewId) if (udfpsView == null) { udfpsView = - DeviceEntryIconView(view.context, null).apply { + DeviceEntryIconView(view.context, null, logger = logger).apply { id = udfpsViewId contentDescription = context.resources.getString( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 4d6577c0423a..b951b736abf2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.content.res.ColorStateList +import android.util.Log import android.util.StateSet import android.view.HapticFeedbackConstants import android.view.View @@ -83,6 +84,11 @@ object DeviceEntryIconViewBinder { if ( !isA11yAction && falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY) ) { + Log.d( + TAG, + "Long press rejected because it is not a11yAction " + + "and it is a falseLongTap" + ) return } vibratorHelper.performHapticFeedback( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt index 3e6d5da38257..8d2e939da032 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt @@ -23,6 +23,7 @@ import android.util.AttributeSet import android.util.StateSet import android.view.Gravity import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.widget.FrameLayout @@ -31,6 +32,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieDrawable import com.android.systemui.common.ui.view.LongPressHandlingView +import com.android.systemui.log.LongPressHandlingViewLogger import com.android.systemui.res.R class DeviceEntryIconView @@ -39,8 +41,17 @@ constructor( context: Context, attrs: AttributeSet?, defStyleAttrs: Int = 0, + logger: LongPressHandlingViewLogger? = null, ) : FrameLayout(context, attrs, defStyleAttrs) { - val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs) + + val longPressHandlingView: LongPressHandlingView = + LongPressHandlingView( + context = context, + attrs = attrs, + longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() }, + allowedTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(), + logger = logger, + ) val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg } val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg } val aodFpDrawable: LottieDrawable = LottieDrawable() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 51230dd0a47c..782d37b1929c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -42,6 +42,9 @@ import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LongPressHandlingViewLogger +import com.android.systemui.log.dagger.LongPressTouchLog import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView @@ -69,6 +72,7 @@ constructor( private val deviceEntryBackgroundViewModel: Lazy<DeviceEntryBackgroundViewModel>, private val falsingManager: Lazy<FalsingManager>, private val vibratorHelper: Lazy<VibratorHelper>, + @LongPressTouchLog private val logBuffer: LogBuffer, ) : KeyguardSection() { private val deviceEntryIconViewId = R.id.device_entry_icon_view private var disposableHandle: DisposableHandle? = null @@ -88,7 +92,16 @@ constructor( val view = if (DeviceEntryUdfpsRefactor.isEnabled) { - DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId } + DeviceEntryIconView( + context, + null, + logger = + LongPressHandlingViewLogger( + logBuffer = logBuffer, + TAG + ) + ) + .apply { id = deviceEntryIconViewId } } else { // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled LockIconView(context, null).apply { id = R.id.lock_icon_view } @@ -258,4 +271,8 @@ constructor( } } } + + companion object { + private const val TAG = "DefaultDeviceEntrySection" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt index b432417802c9..9f8e9c575a75 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt @@ -18,6 +18,9 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LongPressHandlingViewLogger +import com.android.systemui.log.dagger.LongPressTouchLog import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.gesture.TapGestureDetector import dagger.Lazy @@ -37,4 +40,11 @@ constructor( Lazy<AlternateBouncerUdfpsAccessibilityOverlayViewModel>, val messageAreaViewModel: AlternateBouncerMessageAreaViewModel, val powerInteractor: PowerInteractor, -) + @LongPressTouchLog private val touchLogBuffer: LogBuffer, +) { + val logger: LongPressHandlingViewLogger = + LongPressHandlingViewLogger(logBuffer = touchLogBuffer, TAG) + companion object { + private const val TAG = "AlternateBouncer" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt index f33752fc04d4..12bcc7ecbab8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.MathUtils +import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -55,8 +56,18 @@ constructor( var currentAlpha = 0f return transitionAnimation.sharedFlow( duration = 250.milliseconds, - startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements. - onStart = { currentAlpha = viewState.alpha() }, + startTime = if (lightRevealMigration()) { + 100.milliseconds // Wait for the light reveal to "hit" the LS elements. + } else { + 0.milliseconds + }, + onStart = { + if (lightRevealMigration()) { + currentAlpha = viewState.alpha() + } else { + currentAlpha = 0f + } + }, onStep = { MathUtils.lerp(currentAlpha, 0f, it) }, onCancel = { 0f }, ) diff --git a/packages/SystemUI/src/com/android/systemui/log/LongPressHandlingViewLogger.kt b/packages/SystemUI/src/com/android/systemui/log/LongPressHandlingViewLogger.kt new file mode 100644 index 000000000000..4ff81184d045 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/LongPressHandlingViewLogger.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log + +import com.android.systemui.log.core.LogLevel.DEBUG +import com.google.errorprone.annotations.CompileTimeConstant + +data class LongPressHandlingViewLogger +constructor( + private val logBuffer: LogBuffer, + @CompileTimeConstant private val tag: String = "LongPressHandlingViewLogger" +) { + fun schedulingLongPress(delay: Long) { + logBuffer.log( + tag, + DEBUG, + { long1 = delay }, + { "on MotionEvent.Down: scheduling long press activation after $long1 ms" } + ) + } + + fun longPressTriggered() { + logBuffer.log(tag, DEBUG, "long press event detected and dispatched") + } + + fun motionEventCancelled() { + logBuffer.log(tag, DEBUG, "Long press may be cancelled due to MotionEventModel.Cancel") + } + + fun dispatchingSingleTap() { + logBuffer.log(tag, DEBUG, "Dispatching single tap instead of long press") + } + + fun onUpEvent(distanceMoved: Float, touchSlop: Int, gestureDuration: Long) { + logBuffer.log( + tag, + DEBUG, + { + double1 = distanceMoved.toDouble() + int1 = touchSlop + long1 = gestureDuration + }, + { + "on MotionEvent.Up: distanceMoved: $double1, " + + "allowedTouchSlop: $int1, " + + "eventDuration: $long1" + } + ) + } + + fun cancelingLongPressDueToTouchSlop(distanceMoved: Float, allowedTouchSlop: Int) { + logBuffer.log( + tag, + DEBUG, + { + double1 = distanceMoved.toDouble() + int1 = allowedTouchSlop + }, + { + "on MotionEvent.Motion: May cancel long press due to movement: " + + "distanceMoved: $double1, " + + "allowedTouchSlop: $int1 " + } + ) + } +} 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 19906fdc4d5f..498c34c03f2d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -717,4 +717,12 @@ public class LogModule { public static LogBuffer provideVolumeLogBuffer(LogBufferFactory factory) { return factory.create("VolumeLog", 50); } + + /** Provides a {@link LogBuffer} for use by long touch event handlers. */ + @Provides + @SysUISingleton + @LongPressTouchLog + public static LogBuffer providesLongPressTouchLog(LogBufferFactory factory) { + return factory.create("LongPressViewLog", 200); + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LongPressTouchLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/LongPressTouchLog.kt new file mode 100644 index 000000000000..1163d74b62a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LongPressTouchLog.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** Log buffer for logging touch/long press events */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class LongPressTouchLog diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt index 24c57bea8bec..4ad437c50d61 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -76,6 +76,7 @@ import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider import com.android.systemui.media.controls.ui.view.MediaViewHolder @@ -943,7 +944,7 @@ class LegacyMediaDataManagerImpl( desc.subtitle, desc.title, artworkIcon, - listOf(mediaAction), + listOf(), listOf(0), MediaButton(playOrPause = mediaAction), packageName, @@ -1074,13 +1075,13 @@ class LegacyMediaDataManagerImpl( } // Control buttons - // If flag is enabled and controller has a PlaybackState, create actions from session info + // If controller has a PlaybackState, create actions from session info // Otherwise, use the notification actions - var actionIcons: List<MediaAction> = emptyList() + var actionIcons: List<MediaNotificationAction> = emptyList() var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) if (semanticActions == null) { - val actions = createActionsFromNotification(context, activityStarter, sbn) + val actions = createActionsFromNotification(context, sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second } @@ -1464,7 +1465,7 @@ class LegacyMediaDataManagerImpl( val updated = data.copy( token = null, - actions = actions, + actions = listOf(), semanticActions = MediaButton(playOrPause = resumeAction), actionsToShowInCompact = listOf(0), active = false, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index bcf748e7573f..f2825d0465ad 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -33,6 +33,7 @@ import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManage import com.android.systemui.media.controls.shared.MediaControlDrawables import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState @@ -217,11 +218,10 @@ private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Lo /** Generate action buttons based on notification actions */ fun createActionsFromNotification( context: Context, - activityStarter: ActivityStarter, sbn: StatusBarNotification -): Pair<List<MediaAction>, List<Int>> { +): Pair<List<MediaNotificationAction>, List<Int>> { val notif = sbn.notification - val actionIcons: MutableList<MediaAction> = ArrayList() + val actionIcons: MutableList<MediaNotificationAction> = ArrayList() val actions = notif.actions var actionsToShowCollapsed = notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() @@ -250,25 +250,6 @@ fun createActionsFromNotification( continue } - val runnable = - action.actionIntent?.let { actionIntent -> - Runnable { - when { - actionIntent.isActivity -> - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent - ) - action.isAuthenticationRequired -> - activityStarter.dismissKeyguardThenExecute( - { sendPendingIntent(action.actionIntent) }, - {}, - true - ) - else -> sendPendingIntent(actionIntent) - } - } - } - val themeText = com.android.settingslib.Utils.getColorAttr( context, @@ -285,13 +266,53 @@ fun createActionsFromNotification( .setTint(themeText) .loadDrawable(context) - val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + val mediaAction = + MediaNotificationAction( + action.isAuthenticationRequired, + action.actionIntent, + mediaActionIcon, + action.title + ) actionIcons.add(mediaAction) } } return Pair(actionIcons, actionsToShowCollapsed) } +/** + * Converts [MediaNotificationAction] list into [MediaAction] list + * + * @param actions list of [MediaNotificationAction] + * @param activityStarter starter for activities + * @return list of [MediaAction] + */ +fun getNotificationActions( + actions: List<MediaNotificationAction>, + activityStarter: ActivityStarter +): List<MediaAction> { + return actions.map { action -> + val runnable = + action.actionIntent?.let { actionIntent -> + Runnable { + when { + actionIntent.isActivity -> + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + action.isAuthenticationRequired -> + activityStarter.dismissKeyguardThenExecute( + { sendPendingIntent(action.actionIntent) }, + {}, + true + ) + else -> sendPendingIntent(actionIntent) + } + } + } + MediaAction(action.icon, runnable, action.contentDescription, background = null) + } +} + private fun sendPendingIntent(intent: PendingIntent): Boolean { return try { intent.send( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index f9fef8eac815..53cc15b8c588 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -54,10 +54,10 @@ import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.media.controls.util.MediaControllerFactory import com.android.systemui.media.controls.util.MediaDataUtils import com.android.systemui.media.controls.util.MediaFlags -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.row.HybridGroupManager @@ -80,7 +80,6 @@ constructor( @Application val context: Context, @Main val mainDispatcher: CoroutineDispatcher, @Background val backgroundScope: CoroutineScope, - private val activityStarter: ActivityStarter, private val mediaControllerFactory: MediaControllerFactory, private val mediaFlags: MediaFlags, private val imageLoader: ImageLoader, @@ -209,15 +208,14 @@ constructor( val device: MediaDeviceData? = getDeviceInfoForRemoteCast(key, sbn) // Control buttons - // If flag is enabled and controller has a PlaybackState, create actions from session - // info + // If controller has a PlaybackState, create actions from session info // Otherwise, use the notification actions - var actionIcons: List<MediaAction> = emptyList() + var actionIcons: List<MediaNotificationAction> = emptyList() var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) logD(TAG) { "Semantic actions: $semanticActions" } if (semanticActions == null) { - val actions = createActionsFromNotification(context, activityStarter, sbn) + val actions = createActionsFromNotification(context, sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second logD(TAG) { "[!!] Semantic actions: $semanticActions" } @@ -329,7 +327,7 @@ constructor( artist = desc.subtitle, song = desc.title, artworkIcon = artworkIcon, - actionIcons = listOf(mediaAction), + actionIcons = listOf(), actionsToShowInCompact = listOf(0), semanticActions = MediaButton(playOrPause = mediaAction), token = token, @@ -514,7 +512,7 @@ constructor( val artist: CharSequence?, val song: CharSequence?, val artworkIcon: Icon?, - val actionIcons: List<MediaAction>, + val actionIcons: List<MediaNotificationAction>, val actionsToShowInCompact: List<Int>, val semanticActions: MediaButton?, val token: MediaSession.Token?, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt index 4555810ee0ef..5f0a9f82b9ae 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -71,12 +71,14 @@ import com.android.systemui.media.controls.data.repository.MediaDataRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.MediaLogger import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider import com.android.systemui.media.controls.ui.view.MediaViewHolder @@ -149,6 +151,7 @@ class MediaDataProcessor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val mediaDataRepository: MediaDataRepository, private val mediaDataLoader: dagger.Lazy<MediaDataLoader>, + private val mediaLogger: MediaLogger, ) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -228,6 +231,7 @@ class MediaDataProcessor( keyguardUpdateMonitor: KeyguardUpdateMonitor, mediaDataRepository: MediaDataRepository, mediaDataLoader: dagger.Lazy<MediaDataLoader>, + mediaLogger: MediaLogger, ) : this( context, applicationScope, @@ -253,6 +257,7 @@ class MediaDataProcessor( keyguardUpdateMonitor, mediaDataRepository, mediaDataLoader, + mediaLogger, ) private val appChangeReceiver = @@ -794,7 +799,7 @@ class MediaDataProcessor( desc.subtitle, desc.title, artworkIcon, - listOf(mediaAction), + listOf(), listOf(0), MediaButton(playOrPause = mediaAction), packageName, @@ -832,12 +837,48 @@ class MediaDataProcessor( return@withContext } - val currentEntry = mediaDataRepository.mediaEntries.value[key] - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L - val resumeAction: Runnable? = currentEntry?.resumeAction - val hasCheckedForResume = currentEntry?.hasCheckedForResume == true - val active = currentEntry?.active ?: true + val mediaController = mediaControllerFactory.create(result.token!!) + val oldEntry = mediaDataRepository.mediaEntries.value[key] + val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId() + val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L + val resumeAction: Runnable? = oldEntry?.resumeAction + val hasCheckedForResume = oldEntry?.hasCheckedForResume == true + val active = oldEntry?.active ?: true + + val mediaData = + MediaData( + userId = sbn.normalizedUserId, + initialized = true, + app = result.appName, + appIcon = result.appIcon, + artist = result.artist, + song = result.song, + artwork = result.artworkIcon, + actions = result.actionIcons, + actionsToShowInCompact = result.actionsToShowInCompact, + semanticActions = result.semanticActions, + packageName = sbn.packageName, + token = result.token, + clickIntent = result.clickIntent, + device = result.device, + active = active, + resumeAction = resumeAction, + playbackLocation = result.playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = result.isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = result.appUid, + isExplicit = result.isExplicit, + ) + + if (isSameMediaData(context, mediaController, mediaData, oldEntry)) { + mediaLogger.logDuplicateMediaNotification(key) + return@withContext + } // We need to log the correct media added. if (isNewlyActiveEntry) { @@ -848,7 +889,7 @@ class MediaDataProcessor( instanceId, result.playbackLocation ) - } else if (result.playbackLocation != currentEntry?.playbackLocation) { + } else if (result.playbackLocation != oldEntry?.playbackLocation) { logger.logPlaybackLocationChange( result.appUid, sbn.packageName, @@ -857,40 +898,7 @@ class MediaDataProcessor( ) } - withContext(mainDispatcher) { - onMediaDataLoaded( - key, - oldKey, - MediaData( - userId = sbn.normalizedUserId, - initialized = true, - app = result.appName, - appIcon = result.appIcon, - artist = result.artist, - song = result.song, - artwork = result.artworkIcon, - actions = result.actionIcons, - actionsToShowInCompact = result.actionsToShowInCompact, - semanticActions = result.semanticActions, - packageName = sbn.packageName, - token = result.token, - clickIntent = result.clickIntent, - device = result.device, - active = active, - resumeAction = resumeAction, - playbackLocation = result.playbackLocation, - notificationKey = key, - hasCheckedForResume = hasCheckedForResume, - isPlaying = result.isPlaying, - isClearable = !sbn.isOngoing, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = result.appUid, - isExplicit = result.isExplicit, - ) - ) - } + withContext(mainDispatcher) { onMediaDataLoaded(key, oldKey, mediaData) } } @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up") @@ -1001,13 +1009,13 @@ class MediaDataProcessor( } // Control buttons - // If flag is enabled and controller has a PlaybackState, create actions from session info + // If controller has a PlaybackState, create actions from session info // Otherwise, use the notification actions - var actionIcons: List<MediaAction> = emptyList() + var actionIcons: List<MediaNotificationAction> = emptyList() var actionsToShowCollapsed: List<Int> = emptyList() val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) if (semanticActions == null) { - val actions = createActionsFromNotification(context, activityStarter, sbn) + val actions = createActionsFromNotification(context, sbn) actionIcons = actions.first actionsToShowCollapsed = actions.second } @@ -1022,57 +1030,72 @@ class MediaDataProcessor( else MediaData.PLAYBACK_CAST_LOCAL val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } - val currentEntry = mediaDataRepository.mediaEntries.value.get(key) - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val oldEntry = mediaDataRepository.mediaEntries.value.get(key) + val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId() val appUid = appInfo?.uid ?: Process.INVALID_UID + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L + val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction + val hasCheckedForResume = + mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true + val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true + var mediaData = + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), + ) + + if (isSameMediaData(context, mediaController, mediaData, oldEntry)) { + mediaLogger.logDuplicateMediaNotification(key) + return + } + if (isNewlyActiveEntry) { logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) - } else if (playbackLocation != currentEntry?.playbackLocation) { + } else if (playbackLocation != oldEntry?.playbackLocation) { logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) } - val lastActive = systemClock.elapsedRealtime() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L foregroundExecutor.execute { - val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction - val hasCheckedForResume = + val oldResumeAction: Runnable? = + mediaDataRepository.mediaEntries.value[key]?.resumeAction + val oldHasCheckedForResume = mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true - val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true - onMediaDataLoaded( - key, - oldKey, - MediaData( - sbn.normalizedUserId, - true, - appName, - smallIcon, - artist, - song, - artWorkIcon, - actionIcons, - actionsToShowCollapsed, - semanticActions, - sbn.packageName, - token, - notif.contentIntent, - device, - active, - resumeAction = resumeAction, - playbackLocation = playbackLocation, - notificationKey = key, - hasCheckedForResume = hasCheckedForResume, - isPlaying = isPlaying, - isClearable = !sbn.isOngoing, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = appUid, - isExplicit = isExplicit, - smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), + val oldActive = mediaDataRepository.mediaEntries.value[key]?.active ?: true + mediaData = + mediaData.copy( + resumeAction = oldResumeAction, + hasCheckedForResume = oldHasCheckedForResume, + active = oldActive ) - ) + onMediaDataLoaded(key, oldKey, mediaData) } } @@ -1402,7 +1425,7 @@ class MediaDataProcessor( val updated = data.copy( token = null, - actions = actions, + actions = listOf(), semanticActions = MediaButton(playOrPause = resumeAction), actionsToShowInCompact = listOf(0), active = false, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt new file mode 100644 index 000000000000..55d7b1d498e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 The Android Open 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.media.controls.domain.pipeline + +import android.annotation.WorkerThread +import android.app.PendingIntent +import android.content.Context +import android.graphics.drawable.Icon +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.util.Log +import com.android.systemui.Flags.mediaControlsPostsOptimization +import com.android.systemui.biometrics.Utils.toBitmap +import com.android.systemui.media.controls.shared.model.MediaData + +private const val TAG = "MediaProcessingHelper" + +/** + * Compares [new] media data to [old] media data. + * + * @param context Context + * @param newController media controller of the new media data. + * @param new new media data. + * @param old old media data. + * @return whether new and old contain same data + */ +fun isSameMediaData( + context: Context, + newController: MediaController, + new: MediaData, + old: MediaData? +): Boolean { + if (old == null || !mediaControlsPostsOptimization()) return false + + return new.userId == old.userId && + new.app == old.app && + new.artist == old.artist && + new.song == old.song && + new.packageName == old.packageName && + new.isExplicit == old.isExplicit && + new.appUid == old.appUid && + new.notificationKey == old.notificationKey && + new.isPlaying == old.isPlaying && + new.isClearable == old.isClearable && + new.playbackLocation == old.playbackLocation && + new.device == old.device && + new.initialized == old.initialized && + new.resumption == old.resumption && + new.token == old.token && + new.resumeProgress == old.resumeProgress && + areClickIntentsEqual(new.clickIntent, old.clickIntent) && + areActionsEqual(context, newController, new, old) && + areIconsEqual(context, new.artwork, old.artwork) && + areIconsEqual(context, new.appIcon, old.appIcon) +} + +/** Returns whether actions lists are equal. */ +fun areCustomActionListsEqual( + first: List<PlaybackState.CustomAction>?, + second: List<PlaybackState.CustomAction>? +): Boolean { + // Same object, or both null + if (first === second) { + return true + } + + // Only one null, or different number of actions + if ((first == null || second == null) || (first.size != second.size)) { + return false + } + + // Compare individual actions + first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) -> + if (!areCustomActionsEqual(firstAction, secondAction)) { + return false + } + } + return true +} + +private fun areCustomActionsEqual( + firstAction: PlaybackState.CustomAction, + secondAction: PlaybackState.CustomAction +): Boolean { + if ( + firstAction.action != secondAction.action || + firstAction.name != secondAction.name || + firstAction.icon != secondAction.icon + ) { + return false + } + + if ((firstAction.extras == null) != (secondAction.extras == null)) { + return false + } + if (firstAction.extras != null) { + firstAction.extras.keySet().forEach { key -> + if (firstAction.extras[key] != secondAction.extras[key]) { + return false + } + } + } + return true +} + +@WorkerThread +private fun areIconsEqual(context: Context, new: Icon?, old: Icon?): Boolean { + if (new == old) return true + if (new == null || old == null || new.type != old.type) return false + return if (new.type == Icon.TYPE_BITMAP || new.type == Icon.TYPE_ADAPTIVE_BITMAP) { + if (new.bitmap.isRecycled || old.bitmap.isRecycled) { + Log.e(TAG, "Cannot compare recycled bitmap") + return false + } + new.bitmap.sameAs(old.bitmap) + } else { + val newDrawable = new.loadDrawable(context) + val oldDrawable = old.loadDrawable(context) + + return newDrawable?.toBitmap()?.sameAs(oldDrawable?.toBitmap()) ?: false + } +} + +private fun areActionsEqual( + context: Context, + newController: MediaController, + new: MediaData, + old: MediaData +): Boolean { + val oldState = MediaController(context, old.token!!).playbackState + return if ( + new.semanticActions == null && + old.semanticActions == null && + new.actions.size == old.actions.size + ) { + var same = true + new.actions.asSequence().zip(old.actions.asSequence()).forEach { + if ( + it.first.actionIntent?.intent?.filterEquals(it.second.actionIntent?.intent) != + true || + it.first.icon != it.second.icon || + it.first.contentDescription != it.second.contentDescription + ) { + same = false + return@forEach + } + } + same + } else if (new.semanticActions != null && old.semanticActions != null) { + oldState?.actions == newController.playbackState?.actions && + areCustomActionListsEqual( + oldState?.customActions, + newController.playbackState?.customActions + ) + } else { + false + } +} + +private fun areClickIntentsEqual(newIntent: PendingIntent?, oldIntent: PendingIntent?): Boolean { + if ((newIntent == null && oldIntent == null) || newIntent === oldIntent) return true + if (newIntent == null || oldIntent == null) return false + + return newIntent.intent?.filterEquals(oldIntent.intent) == true +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt index fc319036d67e..275f1eecd4db 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt @@ -16,12 +16,14 @@ package com.android.systemui.media.controls.domain.pipeline +import android.annotation.WorkerThread import android.media.session.MediaController import android.media.session.MediaSession import android.media.session.PlaybackState import android.os.SystemProperties import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData @@ -32,6 +34,7 @@ import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.time.SystemClock +import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -49,6 +52,8 @@ class MediaTimeoutListener @Inject constructor( private val mediaControllerFactory: MediaControllerFactory, + @Background private val bgExecutor: Executor, + @Main private val uiExecutor: Executor, @Main private val mainExecutor: DelayableExecutor, private val logger: MediaTimeoutLogger, statusBarStateController: SysuiStatusBarStateController, @@ -147,19 +152,21 @@ constructor( } reusedListener?.let { - val wasPlaying = it.isPlaying() - logger.logUpdateListener(key, wasPlaying) - it.setMediaData(data) - it.key = key - mediaListeners[key] = it - if (wasPlaying != it.isPlaying()) { - // If a player becomes active because of a migration, we'll need to broadcast - // its state. Doing it now would lead to reentrant callbacks, so let's wait - // until we're done. - mainExecutor.execute { - if (mediaListeners[key]?.isPlaying() == true) { - logger.logDelayedUpdate(key) - timeoutCallback.invoke(key, false /* timedOut */) + bgExecutor.execute { + val wasPlaying = it.isPlaying() + logger.logUpdateListener(key, wasPlaying) + it.setMediaData(data) + it.key = key + mediaListeners[key] = it + if (wasPlaying != it.isPlaying()) { + // If a player becomes active because of a migration, we'll need to broadcast + // its state. Doing it now would lead to reentrant callbacks, so let's wait + // until we're done. + mainExecutor.execute { + if (mediaListeners[key]?.isPlaying() == true) { + logger.logDelayedUpdate(key) + timeoutCallback.invoke(key, false /* timedOut */) + } } } } @@ -217,18 +224,20 @@ constructor( private set fun Int.isPlaying() = isPlayingState(this) + fun isPlaying() = lastState?.state?.isPlaying() ?: false init { - setMediaData(data) + bgExecutor.execute { setMediaData(data) } } fun destroy() { - mediaController?.unregisterCallback(this) + bgExecutor.execute { mediaController?.unregisterCallback(this) } cancellation?.run() destroyed = true } + @WorkerThread fun setMediaData(data: MediaData) { sessionToken = data.token destroyed = false @@ -258,7 +267,7 @@ constructor( if (resumption == true) { // Some apps create a session when MBS is queried. We should unregister the // controller since it will no longer be valid, but don't cancel the timeout - mediaController?.unregisterCallback(this) + bgExecutor.execute { mediaController?.unregisterCallback(this) } } else { // For active controls, if the session is destroyed, clean up everything since we // will need to recreate it if this key is updated later @@ -284,7 +293,7 @@ constructor( if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) { logger.logStateCallback(key) - stateCallback.invoke(key, state) + uiExecutor.execute { stateCallback.invoke(key, state) } } if (playingStateSame && !resumptionChanged) { @@ -313,7 +322,7 @@ constructor( expireMediaTimeout(key, "playback started - $state, $key") timedOut = false if (dispatchEvents) { - timeoutCallback(key, timedOut) + uiExecutor.execute { timeoutCallback(key, timedOut) } } } } @@ -337,60 +346,13 @@ constructor( } } - private fun areCustomActionListsEqual( - first: List<PlaybackState.CustomAction>?, - second: List<PlaybackState.CustomAction>? - ): Boolean { - // Same object, or both null - if (first === second) { - return true - } - - // Only one null, or different number of actions - if ((first == null || second == null) || (first.size != second.size)) { - return false - } - - // Compare individual actions - first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) -> - if (!areCustomActionsEqual(firstAction, secondAction)) { - return false - } - } - return true - } - - private fun areCustomActionsEqual( - firstAction: PlaybackState.CustomAction, - secondAction: PlaybackState.CustomAction - ): Boolean { - if ( - firstAction.action != secondAction.action || - firstAction.name != secondAction.name || - firstAction.icon != secondAction.icon - ) { - return false - } - - if ((firstAction.extras == null) != (secondAction.extras == null)) { - return false - } - if (firstAction.extras != null) { - firstAction.extras.keySet().forEach { key -> - if (firstAction.extras.get(key) != secondAction.extras.get(key)) { - return false - } - } - } - return true - } - /** Listens to changes in recommendation card data and schedules a timeout for its expiration */ private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) { private var timedOut = false var destroyed = false var expiration = Long.MAX_VALUE private set + var cancellation: Runnable? = null private set diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt index 245f6f8bf2f6..130868dc3c1c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.animation.Expandable import com.android.systemui.bluetooth.BroadcastDialogController import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.getNotificationActions import com.android.systemui.media.controls.shared.model.MediaControlModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.util.MediaSmartspaceLogger @@ -102,7 +103,7 @@ constructor( artwork = artwork, deviceData = device, semanticActionButtons = semanticActions, - notificationActionButtons = actions, + notificationActionButtons = getNotificationActions(data.actions, activityStarter), actionsToShowInCollapsed = actionsToShowInCompact, isDismissible = isClearable, isResume = resumption, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt index 2b710b5a67b7..7d20e170d8bc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt @@ -114,6 +114,15 @@ class MediaLogger @Inject constructor(@MediaLog private val buffer: LogBuffer) { ) } + fun logDuplicateMediaNotification(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "duplicate media notification $str1 posted" } + ) + } + companion object { private const val TAG = "MediaLog" } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt index 40b34779151d..aed86090ef01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt @@ -39,7 +39,7 @@ data class MediaData( /** Album artwork. */ val artwork: Icon? = null, /** List of generic action buttons for the media player, based on notification actions */ - val actions: List<MediaAction> = emptyList(), + val actions: List<MediaNotificationAction> = emptyList(), /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */ val actionsToShowInCompact: List<Int> = emptyList(), /** @@ -162,6 +162,14 @@ data class MediaAction( val rebindId: Int? = null ) +/** State of a media action from notification. */ +data class MediaNotificationAction( + val isAuthenticationRequired: Boolean, + val actionIntent: PendingIntent?, + val icon: Drawable?, + val contentDescription: CharSequence? +) + /** State of the media device. */ data class MediaDeviceData @JvmOverloads diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 87610cf774a3..8bec46abd504 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -21,6 +21,7 @@ import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; import static com.android.settingslib.flags.Flags.legacyLeAudioSharing; import static com.android.systemui.Flags.communalHub; import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation; +import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions; import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS; import android.animation.Animator; @@ -1170,7 +1171,7 @@ public class MediaControlPanel { // Set all the generic buttons List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); - List<MediaAction> actions = data.getActions(); + List<MediaAction> actions = getNotificationActions(data.getActions(), mActivityStarter); int i = 0; for (; i < actions.size() && i < genericButtons.size(); i++) { boolean showInCompact = actionsWhenCollapsed.contains(i); diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java index e8c90c1fc9bf..c70a5234dfca 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java @@ -34,7 +34,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; -import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS; import static com.android.systemui.navigationbar.NavBarHelper.transitionMode; import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; @@ -60,7 +59,6 @@ import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.StatusBarManager; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.PixelFormat; @@ -105,7 +103,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.app.viewcapture.ViewCaptureAwareWindowManager; -import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -1625,11 +1622,9 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private boolean onAccessibilityLongClick(View v) { - final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - final String chooserClassName = AccessibilityButtonChooserActivity.class.getName(); - intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); - mContext.startActivityAsUser(intent, mUserTracker.getUserHandle()); + final Display display = v.getDisplay(); + mAccessibilityManager.notifyAccessibilityButtonLongClicked( + display != null ? display.getDisplayId() : mDisplayTracker.getDefaultDisplayId()); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt index a5c07bc2fdbf..11854d9317c9 100644 --- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt @@ -18,9 +18,13 @@ package com.android.systemui.notifications.ui.viewmodel import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,8 +38,10 @@ class NotificationsShadeUserActionsViewModel @AssistedInject constructor() : override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { setActions( mapOf( - Swipe.Up to SceneFamilies.Home, Back to SceneFamilies.Home, + Swipe.Up to SceneFamilies.Home, + Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to + ReplaceByOverlay(Overlays.QuickSettingsShade), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index c39ff557e54f..c2f1c3dcd426 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.composefragment import android.annotation.SuppressLint import android.graphics.Rect import android.os.Bundle +import android.util.IndentingPrintWriter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -41,8 +42,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.dimensionResource @@ -59,7 +60,9 @@ import com.android.compose.modifiers.height import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf import com.android.compose.theme.PlatformTheme +import com.android.systemui.Dumpable import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.dump.DumpManager import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.view.MediaHost @@ -76,6 +79,10 @@ import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.qs.ui.composable.ShadeBody import com.android.systemui.res.R import com.android.systemui.util.LifecycleFragment +import com.android.systemui.util.asIndenting +import com.android.systemui.util.printSection +import com.android.systemui.util.println +import java.io.PrintWriter import java.util.function.Consumer import javax.inject.Inject import javax.inject.Named @@ -91,9 +98,10 @@ class QSFragmentCompose @Inject constructor( private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory, + private val dumpManager: DumpManager, @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost, @Named(QS_PANEL) private val qsMediaHost: MediaHost, -) : LifecycleFragment(), QS { +) : LifecycleFragment(), QS, Dumpable { private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null) private val heightListener = MutableStateFlow<QS.HeightListener?>(null) @@ -118,8 +126,24 @@ constructor( var top by mutableStateOf(0) var bottom by mutableStateOf(0) var radius by mutableStateOf(0) + + fun dump(pw: IndentingPrintWriter) { + pw.printSection("NotificationScrimClippingParams") { + pw.println("isEnabled", isEnabled) + pw.println("leftInset", "${leftInset}px") + pw.println("rightInset", "${rightInset}px") + pw.println("top", "${top}px") + pw.println("bottom", "${bottom}px") + pw.println("radius", "${radius}px") + } + } } + override fun onStart() { + super.onStart() + registerDumpable() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -343,11 +367,11 @@ constructor( } override fun getHeaderTop(): Int { - return viewModel.qqsHeaderHeight.value + return qqsPositionOnRoot.top } override fun getHeaderBottom(): Int { - return headerTop + qqsHeight.value + return qqsPositionOnRoot.bottom } override fun getHeaderLeft(): Int { @@ -358,7 +382,7 @@ constructor( outBounds.set(qqsPositionOnRoot) view?.getBoundsOnScreen(composeViewPositionOnScreen) ?: run { composeViewPositionOnScreen.setEmpty() } - qqsPositionOnRoot.offset(composeViewPositionOnScreen.left, composeViewPositionOnScreen.top) + outBounds.offset(composeViewPositionOnScreen.left, composeViewPositionOnScreen.top) } override fun isHeaderShown(): Boolean { @@ -404,37 +428,29 @@ constructor( onDispose { qqsVisible.value = false } } Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) { - Box(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = + Modifier.fillMaxWidth() + .onPlaced { coordinates -> + val (leftFromRoot, topFromRoot) = coordinates.positionInRoot().round() + qqsPositionOnRoot.set( + leftFromRoot, + topFromRoot, + leftFromRoot + coordinates.size.width, + topFromRoot + coordinates.size.height, + ) + } + .onSizeChanged { size -> qqsHeight.value = size.height } + .padding(top = { qqsPadding }) + ) { val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() if (qsEnabled) { QuickQuickSettings( viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, modifier = - Modifier.onGloballyPositioned { coordinates -> - val (leftFromRoot, topFromRoot) = - coordinates.positionInRoot().round() - val (width, height) = coordinates.size - qqsPositionOnRoot.set( - leftFromRoot, - topFromRoot, - leftFromRoot + width, - topFromRoot + height - ) - } - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - qqsHeight.value = placeable.height - - layout(placeable.width, placeable.height) { - placeable.place(0, 0) - } - } - .padding(top = { qqsPadding }) - .collapseExpandSemanticAction( - stringResource( - id = R.string.accessibility_quick_settings_expand - ) - ) + Modifier.collapseExpandSemanticAction( + stringResource(id = R.string.accessibility_quick_settings_expand) + ) ) } } @@ -486,6 +502,44 @@ constructor( } } ?: this } + + private fun registerDumpable() { + val instanceId = instanceProvider.getNextId() + // Add an instanceId because the system may have more than 1 of these when re-inflating and + // DumpManager doesn't like repeated identifiers. Also, put it first because DumpHandler + // matches by end. + val stringId = "$instanceId-QSFragmentCompose" + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { + try { + dumpManager.registerNormalDumpable(stringId, this@QSFragmentCompose) + awaitCancellation() + } finally { + dumpManager.unregisterDumpable(stringId) + } + } + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.asIndenting().run { + notificationScrimClippingParams.dump(this) + printSection("QQS positioning") { + println("qqsHeight", "${headerHeight}px") + println("qqsTop", "${headerTop}px") + println("qqsBottom", "${headerBottom}px") + println("qqsLeft", "${headerLeft}px") + println("qqsPositionOnRoot", qqsPositionOnRoot) + val rect = Rect() + getHeaderBoundsOnScreen(rect) + println("qqsPositionOnScreen", rect) + } + println("QQS visible", qqsVisible.value) + if (::viewModel.isInitialized) { + printSection("View Model") { viewModel.dump(this@run, args) } + } + } + } } private fun View.setBackPressedDispatcher() { @@ -526,3 +580,12 @@ private suspend inline fun <Listener : Any, Data> setListenerJob( } } } + +private val instanceProvider = + object { + private var currentId = 0 + + fun getNextId(): Int { + return currentId++ + } + } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 16133f482f7b..7ab11d22ee49 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -21,6 +21,7 @@ import android.graphics.Rect import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleCoroutineScope +import com.android.systemui.Dumpable import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -34,10 +35,14 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.util.LargeScreenUtils +import com.android.systemui.util.asIndenting +import com.android.systemui.util.printSection +import com.android.systemui.util.println import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import java.io.PrintWriter import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -62,7 +67,7 @@ constructor( private val configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, @Assisted private val lifecycleScope: LifecycleCoroutineScope, -) { +) : Dumpable { val footerActionsViewModel = footerActionsViewModelFactory.create(lifecycleScope).also { lifecycleScope.launch { footerActionsController.init() } @@ -228,6 +233,30 @@ constructor( */ var collapseExpandAccessibilityAction: Runnable? = null + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.asIndenting().run { + printSection("Quick Settings state") { + println("isQSExpanded", isQSExpanded) + println("isQSVisible", isQSVisible) + println("isQSEnabled", qsEnabled.value) + println("isCustomizing", containerViewModel.editModeViewModel.isEditing.value) + } + printSection("Expansion state") { + println("qsExpansion", qsExpansionValue) + println("panelExpansionFraction", panelExpansionFractionValue) + println("squishinessFraction", squishinessFractionValue) + println("expansionState", expansionState.value) + } + printSection("Shade state") { + println("stackOverscrolling", stackScrollerOverscrollingValue) + println("statusBarState", StatusBarState.toString(statusBarState.value)) + println("isSmallScreen", isSmallScreenValue) + println("heightOverride", "${heightOverrideValue}px") + println("qqsHeaderHeight", "${qqsHeaderHeight.value}px") + } + } + } + @AssistedFactory interface Factory { fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index cbcf68c27bf8..2f843ac610a3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -50,10 +50,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.util.concurrency.DelayableExecutor; +import com.android.systemui.util.time.SystemClock; import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; @@ -95,6 +97,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements // Bind retry control. private static final int MAX_BIND_RETRIES = 5; private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS; + private static final long ACTIVE_TILE_BIND_RETRY_DELAY = 1 * DateUtils.SECOND_IN_MILLIS; private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS; private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000; private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION = @@ -107,6 +110,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements private final Intent mIntent; private final UserHandle mUser; private final DelayableExecutor mExecutor; + private final SystemClock mSystemClock; private final IBinder mToken = new Binder(); private final PackageManagerAdapter mPackageManagerAdapter; private final BroadcastDispatcher mBroadcastDispatcher; @@ -120,7 +124,6 @@ public class TileLifecycleManager extends BroadcastReceiver implements private IBinder mClickBinder; private int mBindTryCount; - private long mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY; private AtomicBoolean isDeathRebindScheduled = new AtomicBoolean(false); private AtomicBoolean mBound = new AtomicBoolean(false); private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false); @@ -138,7 +141,8 @@ public class TileLifecycleManager extends BroadcastReceiver implements TileLifecycleManager(@Main Handler handler, Context context, IQSService service, PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher, @Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager, - IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) { + IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor, + SystemClock systemClock) { mContext = context; mHandler = handler; mIntent = intent; @@ -146,6 +150,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements mIntent.putExtra(TileService.EXTRA_TOKEN, mToken); mUser = user; mExecutor = executor; + mSystemClock = systemClock; mPackageManagerAdapter = packageManagerAdapter; mBroadcastDispatcher = broadcastDispatcher; mActivityManager = activityManager; @@ -436,25 +441,31 @@ public class TileLifecycleManager extends BroadcastReceiver implements // If mBound is true (meaning that we should be bound), then reschedule binding for // later. if (mBound.get() && checkComponentState()) { - if (isDeathRebindScheduled.compareAndSet(false, true)) { + if (isDeathRebindScheduled.compareAndSet(false, true)) { // if already not scheduled + + mExecutor.executeDelayed(() -> { // Only rebind if we are supposed to, but remove the scheduling anyway. if (mBound.get()) { setBindService(true); } - isDeathRebindScheduled.set(false); + isDeathRebindScheduled.set(false); // allow scheduling again }, getRebindDelay()); } } }); } + private long mLastRebind = 0; /** * @return the delay to automatically rebind after a service died. It provides a longer delay if * the device is a low memory state because the service is likely to get killed again by the * system. In this case we want to rebind later and not to cause a loop of a frequent rebinds. + * It also provides a longer delay if called quickly (a few seconds) after a first call. */ private long getRebindDelay() { + final long now = mSystemClock.currentTimeMillis(); + final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); mActivityManager.getMemoryInfo(info); @@ -462,7 +473,20 @@ public class TileLifecycleManager extends BroadcastReceiver implements if (info.lowMemory) { delay = LOW_MEMORY_BIND_RETRY_DELAY; } else { - delay = mBindRetryDelay; + if (Flags.qsQuickRebindActiveTiles()) { + final long elapsedTimeSinceLastRebind = now - mLastRebind; + final boolean justAttemptedRebind = + elapsedTimeSinceLastRebind < DEFAULT_BIND_RETRY_DELAY; + if (isActiveTile() && !justAttemptedRebind) { + delay = ACTIVE_TILE_BIND_RETRY_DELAY; + } else { + delay = DEFAULT_BIND_RETRY_DELAY; + } + } else { + delay = DEFAULT_BIND_RETRY_DELAY; + } + + mLastRebind = now; } if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent()); return delay; diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java index d10471d86d0b..c5fa8cf05fd0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java @@ -44,7 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * Manages the priority which lets {@link TileServices} make decisions about which tiles * to bind. Also holds on to and manages the {@link TileLifecycleManager}, informing it - * of when it is allowed to bind based on decisions frome the {@link TileServices}. + * of when it is allowed to bind based on decisions from the {@link TileServices}. */ public class TileServiceManager { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java index b96e83d43e32..f723ff264e0c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java @@ -16,6 +16,8 @@ package com.android.systemui.qs.tiles; +import static com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.LAUNCH_SOURCE_QS_TILE; + import android.content.Intent; import android.os.Handler; import android.os.Looper; @@ -96,7 +98,7 @@ public class HearingDevicesTile extends QSTileImpl<BooleanState> { @Override protected void handleClick(@Nullable Expandable expandable) { - mUiHandler.post(() -> mDialogManager.showDialog(expandable)); + mUiHandler.post(() -> mDialogManager.showDialog(expandable, LAUNCH_SOURCE_QS_TILE)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index a3feb2b09da3..d89e73d2c69f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -43,12 +43,14 @@ import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.recordissue.IssueRecordingService +import com.android.systemui.recordissue.IssueRecordingService.Companion.getStartIntent +import com.android.systemui.recordissue.IssueRecordingService.Companion.getStopIntent import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC import com.android.systemui.recordissue.TraceurMessageSender import com.android.systemui.res.R +import com.android.systemui.screenrecord.RecordingController import com.android.systemui.screenrecord.RecordingService import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil @@ -56,6 +58,9 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import java.util.concurrent.Executor import javax.inject.Inject +const val DELAY_MS: Long = 0 +const val INTERVAL_MS: Long = 1000 + class RecordIssueTile @Inject constructor( @@ -77,6 +82,7 @@ constructor( @Background private val bgExecutor: Executor, private val issueRecordingState: IssueRecordingState, private val delegateFactory: RecordIssueDialogDelegate.Factory, + private val recordingController: RecordingController, ) : QSTileImpl<QSTile.BooleanState>( host, @@ -132,23 +138,25 @@ constructor( } private fun startIssueRecordingService() = - PendingIntent.getForegroundService( - userContextProvider.userContext, - RecordingService.REQUEST_CODE, - IssueRecordingService.getStartIntent(userContextProvider.userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + recordingController.startCountdown( + DELAY_MS, + INTERVAL_MS, + pendingServiceIntent(getStartIntent(userContextProvider.userContext)), + pendingServiceIntent(getStopIntent(userContextProvider.userContext)) + ) private fun stopIssueRecordingService() = - PendingIntent.getService( - userContextProvider.userContext, - RecordingService.REQUEST_CODE, - IssueRecordingService.getStopIntent(userContextProvider.userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) + pendingServiceIntent(getStopIntent(userContextProvider.userContext)) .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + private fun pendingServiceIntent(action: Intent) = + PendingIntent.getService( + userContextProvider.userContext, + RecordingService.REQUEST_CODE, + action, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + private fun showPrompt(expandable: Expandable?) { val dialog: AlertDialog = delegateFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt index 4971fefc8f57..0c8a3750f6fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.impl.irecording import android.app.AlertDialog import android.app.BroadcastOptions import android.app.PendingIntent +import android.content.Intent import android.util.Log import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj @@ -27,12 +28,16 @@ import com.android.systemui.animation.Expandable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor +import com.android.systemui.qs.tiles.DELAY_MS +import com.android.systemui.qs.tiles.INTERVAL_MS import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction -import com.android.systemui.recordissue.IssueRecordingService +import com.android.systemui.recordissue.IssueRecordingService.Companion.getStartIntent +import com.android.systemui.recordissue.IssueRecordingService.Companion.getStopIntent import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC +import com.android.systemui.screenrecord.RecordingController import com.android.systemui.screenrecord.RecordingService import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil @@ -53,6 +58,7 @@ constructor( private val panelInteractor: PanelInteractor, private val userContextProvider: UserContextProvider, private val delegateFactory: RecordIssueDialogDelegate.Factory, + private val recordingController: RecordingController, ) : QSTileUserActionInteractor<IssueRecordingModel> { override suspend fun handleInput(input: QSTileInput<IssueRecordingModel>) { @@ -95,20 +101,22 @@ constructor( } private fun startIssueRecordingService() = - PendingIntent.getForegroundService( - userContextProvider.userContext, - RecordingService.REQUEST_CODE, - IssueRecordingService.getStartIntent(userContextProvider.userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + recordingController.startCountdown( + DELAY_MS, + INTERVAL_MS, + pendingServiceIntent(getStartIntent(userContextProvider.userContext)), + pendingServiceIntent(getStopIntent(userContextProvider.userContext)) + ) private fun stopIssueRecordingService() = - PendingIntent.getService( - userContextProvider.userContext, - RecordingService.REQUEST_CODE, - IssueRecordingService.getStopIntent(userContextProvider.userContext), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) + pendingServiceIntent(getStopIntent(userContextProvider.userContext)) .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + + private fun pendingServiceIntent(action: Intent) = + PendingIntent.getService( + userContextProvider.userContext, + RecordingService.REQUEST_CODE, + action, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index c2d112edd65d..483373d8fb6d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -20,13 +20,16 @@ import android.app.Flags import android.content.Context import android.os.UserHandle import com.android.app.tracing.coroutines.flow.map +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.tiles.ModesTile import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes +import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -61,28 +64,39 @@ constructor( suspend fun getCurrentTileModel() = buildTileData(zenModeInteractor.getActiveModes()) private fun buildTileData(activeModes: ActiveZenModes): ModesTileModel { - val modesIconResId = com.android.internal.R.drawable.ic_zen_priority_modes - if (usesModeIcons()) { - val mainModeDrawable = activeModes.mainMode?.icon?.drawable - val iconResId = if (mainModeDrawable == null) modesIconResId else null - + val tileIcon = getTileIcon(activeModes.mainMode) return ModesTileModel( isActivated = activeModes.isAnyActive(), - icon = (mainModeDrawable ?: context.getDrawable(modesIconResId)!!).asIcon(), - iconResId = iconResId, + icon = tileIcon.icon, + iconResId = tileIcon.resId, activeModes = activeModes.modeNames ) } else { return ModesTileModel( isActivated = activeModes.isAnyActive(), - icon = context.getDrawable(modesIconResId)!!.asIcon(), - iconResId = modesIconResId, + icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), + iconResId = ModesTile.ICON_RES_ID, activeModes = activeModes.modeNames ) } } + private data class TileIcon(val icon: Icon.Loaded, val resId: Int?) + + private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon { + return if (activeMode != null) { + // ZenIconKey.resPackage is null if its resId is a system icon. + if (activeMode.icon.key.resPackage == null) { + TileIcon(activeMode.icon.drawable.asIcon(), activeMode.icon.key.resId) + } else { + TileIcon(activeMode.icon.drawable.asIcon(), null) + } + } else { + TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID) + } + } + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi()) private fun usesModeIcons() = Flags.modesApi() && Flags.modesUi() && Flags.modesUiIcons() diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 7f571b135fc8..69da3134314b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -36,9 +36,7 @@ constructor( ) : QSTileDataToStateMapper<ModesTileModel> { override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - if (!android.app.Flags.modesUiIcons()) { - iconRes = data.iconResId - } + iconRes = data.iconResId icon = { data.icon } activationState = if (data.isActivated) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt index d3dc302d44ca..bd1872d455d0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt @@ -18,9 +18,13 @@ package com.android.systemui.qs.ui.viewmodel import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -43,6 +47,13 @@ constructor( .map { editing -> buildMap { put(Swipe.Up, UserActionResult(SceneFamilies.Home)) + put( + Swipe( + direction = SwipeDirection.Down, + fromSource = SceneContainerEdge.TopLeft + ), + ReplaceByOverlay(Overlays.NotificationsShade) + ) if (!editing) { put(Back, UserActionResult(SceneFamilies.Home)) } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 000781acec58..a402a9d5ae7c 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -25,7 +25,6 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; -import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER; @@ -76,7 +75,6 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; -import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.internal.app.IVoiceInteractionSessionListener; @@ -404,23 +402,16 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void notifyAccessibilityButtonClicked(int displayId) { verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonClicked", () -> - AccessibilityManager.getInstance(mContext) - .notifyAccessibilityButtonClicked(displayId)); + AccessibilityManager.getInstance(mContext).notifyAccessibilityButtonClicked( + displayId)); } @Override public void notifyAccessibilityButtonLongClicked() { - verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonLongClicked", - () -> { - final Intent intent = - new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); - final String chooserClassName = AccessibilityButtonChooserActivity - .class.getName(); - intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); - intent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivityAsUser(intent, mUserTracker.getUserHandle()); - }); + verifyCallerAndClearCallingIdentity("notifyAccessibilityButtonLongClicked", () -> + AccessibilityManager.getInstance(mContext) + .notifyAccessibilityButtonLongClicked( + mDisplayTracker.getDefaultDisplayId())); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 3d6d00eb3cc0..a5f4a8959569 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -23,8 +23,6 @@ import android.content.Intent import android.content.res.Resources import android.net.Uri import android.os.Handler -import android.os.UserHandle -import android.provider.Settings import android.util.Log import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogTransitionAnimator @@ -50,11 +48,11 @@ constructor( notificationManager: NotificationManager, userContextProvider: UserContextProvider, keyguardDismissUtil: KeyguardDismissUtil, - private val dialogTransitionAnimator: DialogTransitionAnimator, - private val panelInteractor: PanelInteractor, - private val traceurMessageSender: TraceurMessageSender, + dialogTransitionAnimator: DialogTransitionAnimator, + panelInteractor: PanelInteractor, + traceurMessageSender: TraceurMessageSender, private val issueRecordingState: IssueRecordingState, - private val iActivityManager: IActivityManager, + iActivityManager: IActivityManager, ) : RecordingService( controller, @@ -66,6 +64,18 @@ constructor( keyguardDismissUtil ) { + private val commandHandler = + IssueRecordingServiceCommandHandler( + bgExecutor, + dialogTransitionAnimator, + panelInteractor, + traceurMessageSender, + issueRecordingState, + iActivityManager, + notificationManager, + userContextProvider, + ) + override fun getTag(): String = TAG override fun getChannelId(): String = CHANNEL_ID @@ -76,10 +86,7 @@ constructor( Log.d(getTag(), "handling action: ${intent?.action}") when (intent?.action) { ACTION_START -> { - bgExecutor.execute { - traceurMessageSender.startTracing(issueRecordingState.traceConfig) - } - issueRecordingState.isRecording = true + commandHandler.handleStartCommand() if (!issueRecordingState.recordScreen) { // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action // will circumvent the RecordingService's screen recording start code. @@ -87,41 +94,13 @@ constructor( } } ACTION_STOP, - ACTION_STOP_NOTIF -> { - // ViewCapture needs to save it's data before it is disabled, or else the data will - // be lost. This is expected to change in the near future, and when that happens - // this line should be removed. - bgExecutor.execute { - if (issueRecordingState.traceConfig.longTrace) { - Settings.Global.putInt( - contentResolver, - NOTIFY_SESSION_ENDED_SETTING, - DISABLED - ) - } - traceurMessageSender.stopTracing() - } - issueRecordingState.isRecording = false - } + ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver) ACTION_SHARE -> { - bgExecutor.execute { - mNotificationManager.cancelAsUser( - null, - intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId), - UserHandle(mUserContextTracker.userContext.userId) - ) - - val screenRecording = intent.getParcelableExtra(EXTRA_PATH, Uri::class.java) - if (issueRecordingState.takeBugreport) { - iActivityManager.requestBugReportWithExtraAttachment(screenRecording) - } else { - traceurMessageSender.shareTraces(applicationContext, screenRecording) - } - } - - dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() - panelInteractor.collapsePanels() - + commandHandler.handleShareCommand( + intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId), + intent.getParcelableExtra(EXTRA_PATH, Uri::class.java), + this + ) // Unlike all other actions, action_share has different behavior for the screen // recording qs tile than it does for the record issue qs tile. Return sticky to // avoid running any of the base class' code for this action. @@ -135,8 +114,6 @@ constructor( companion object { private const val TAG = "IssueRecordingService" private const val CHANNEL_ID = "issue_record" - private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended" - private const val DISABLED = 0 /** * Get an intent to stop the issue recording service. diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt new file mode 100644 index 000000000000..32de0f353502 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open 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.recordissue + +import android.app.IActivityManager +import android.app.NotificationManager +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.UserHandle +import android.provider.Settings +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor +import com.android.systemui.settings.UserContextProvider +import java.util.concurrent.Executor + +private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended" +private const val DISABLED = 0 + +/** + * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android + * specifically calls out that there is no supported way to test IntentServices here: + * https://developer.android.com/training/testing/other-components/services + */ +class IssueRecordingServiceCommandHandler( + private val bgExecutor: Executor, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val panelInteractor: PanelInteractor, + private val traceurMessageSender: TraceurMessageSender, + private val issueRecordingState: IssueRecordingState, + private val iActivityManager: IActivityManager, + private val notificationManager: NotificationManager, + private val userContextProvider: UserContextProvider, +) { + + fun handleStartCommand() { + bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) } + issueRecordingState.isRecording = true + } + + fun handleStopCommand(contentResolver: ContentResolver) { + bgExecutor.execute { + if (issueRecordingState.traceConfig.longTrace) { + Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED) + } + traceurMessageSender.stopTracing() + } + issueRecordingState.isRecording = false + } + + fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) { + bgExecutor.execute { + notificationManager.cancelAsUser( + null, + notificationId, + UserHandle(userContextProvider.userContext.userId) + ) + + if (issueRecordingState.takeBugreport) { + iActivityManager.requestBugReportWithExtraAttachment(screenRecording) + } else { + traceurMessageSender.shareTraces(context, screenRecording) + } + } + + dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() + panelInteractor.collapsePanels() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 00944b8d0849..834db98263f5 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene +import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.flag.DualShade import dagger.Binds import dagger.Module @@ -119,5 +122,15 @@ interface KeyguardlessSceneContainerFrameworkModule { .mapValues { checkNotNull(it.value) } ) } + + @Provides + fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { + return SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to + // replace this constant with dynamic window insets. + edgeSize = 40.dp + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 4061ad851f57..a4c7d00d0e80 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene +import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.flag.DualShade import dagger.Binds import dagger.Module @@ -129,5 +132,15 @@ interface SceneContainerFrameworkModule { .mapValues { checkNotNull(it.value) } ) } + + @Provides + fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { + return SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to + // replace this constant with dynamic window insets. + edgeSize = 40.dp + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt index 99e554ea5595..a3132736fe33 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt @@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import dagger.Binds import dagger.Module @@ -38,17 +38,17 @@ class NotifShadeSceneFamilyResolver @Inject constructor( @Application applicationScope: CoroutineScope, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : SceneResolver { override val targetFamily: SceneKey = SceneFamilies.NotifShade override val resolvedScene: StateFlow<SceneKey> = - shadeInteractor.shadeMode + shadeModeInteractor.shadeMode .map(::notifShadeScene) .stateIn( applicationScope, started = SharingStarted.Eagerly, - initialValue = notifShadeScene(shadeInteractor.shadeMode.value), + initialValue = notifShadeScene(shadeModeInteractor.shadeMode.value), ) override fun includesScene(scene: SceneKey): Boolean = scene in notifShadeScenes @@ -61,11 +61,7 @@ constructor( } companion object { - val notifShadeScenes = - setOf( - Scenes.NotificationsShade, - Scenes.Shade, - ) + val notifShadeScenes = setOf(Scenes.NotificationsShade, Scenes.Shade) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt index 2962a3ec903d..923e712af15d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt @@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import dagger.Binds import dagger.Module @@ -38,17 +38,17 @@ class QuickSettingsSceneFamilyResolver @Inject constructor( @Application applicationScope: CoroutineScope, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : SceneResolver { override val targetFamily: SceneKey = SceneFamilies.QuickSettings override val resolvedScene: StateFlow<SceneKey> = - shadeInteractor.shadeMode + shadeModeInteractor.shadeMode .map(::quickSettingsScene) .stateIn( applicationScope, started = SharingStarted.Eagerly, - initialValue = quickSettingsScene(shadeInteractor.shadeMode.value), + initialValue = quickSettingsScene(shadeModeInteractor.shadeMode.value), ) override fun includesScene(scene: SceneKey): Boolean = scene in quickSettingsScenes @@ -62,11 +62,7 @@ constructor( companion object { val quickSettingsScenes = - setOf( - Scenes.QuickSettings, - Scenes.QuickSettingsShade, - Scenes.Shade, - ) + setOf(Scenes.QuickSettings, Scenes.QuickSettingsShade, Scenes.Shade) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index e251c9edb57f..98907b037d85 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -22,7 +22,9 @@ import android.app.StatusBarManager import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.UiEventLogger +import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable +import com.android.systemui.Flags import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -73,6 +75,8 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.printSection import com.android.systemui.util.println +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer import dagger.Lazy import java.io.PrintWriter import java.util.Optional @@ -139,10 +143,13 @@ constructor( private val statusBarStateController: SysuiStatusBarStateController, private val alternateBouncerInteractor: AlternateBouncerInteractor, private val vibratorHelper: VibratorHelper, + private val msdlPlayer: MSDLPlayer, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() + private val authInteractionProperties = AuthInteractionProperties() + override fun start() { if (SceneContainerFlag.isEnabled) { sceneLogger.logFrameworkEnabled(isEnabled = true) @@ -541,9 +548,16 @@ constructor( deviceEntryHapticsInteractor.playSuccessHaptic .sample(sceneInteractor.currentScene) .collect { currentScene -> - vibratorHelper.vibrateAuthSuccess( - "$TAG, $currentScene device-entry::success" - ) + if (Flags.msdlFeedback()) { + msdlPlayer.playToken( + MSDLToken.UNLOCK, + authInteractionProperties, + ) + } else { + vibratorHelper.vibrateAuthSuccess( + "$TAG, $currentScene device-entry::success" + ) + } } } @@ -551,9 +565,16 @@ constructor( deviceEntryHapticsInteractor.playErrorHaptic .sample(sceneInteractor.currentScene) .collect { currentScene -> - vibratorHelper.vibrateAuthError( - "$TAG, $currentScene device-entry::error" - ) + if (Flags.msdlFeedback()) { + msdlPlayer.playToken( + MSDLToken.FAILURE, + authInteractionProperties, + ) + } else { + vibratorHelper.vibrateAuthError( + "$TAG, $currentScene device-entry::error" + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 4c6341b672ad..54823945a827 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -19,9 +19,11 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SwipeSourceDetector import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier @@ -33,12 +35,15 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map /** Models UI state for the scene container. */ class SceneContainerViewModel @@ -47,6 +52,8 @@ constructor( private val sceneInteractor: SceneInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, + private val shadeInteractor: ShadeInteractor, + private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : ExclusiveActivatable() { @@ -59,6 +66,20 @@ constructor( /** Whether the container is visible. */ val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible) + /** + * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the + * [UserAction]s for this container. + */ + val edgeDetector: SwipeSourceDetector by + hydrator.hydratedStateOf( + traceName = "edgeDetector", + initialValue = DefaultEdgeDetector, + source = + shadeInteractor.shadeMode.map { + if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector + } + ) + override suspend fun onActivated(): Nothing { try { // Sends a MotionEventHandler to the owner of the view-model so they can report diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt new file mode 100644 index 000000000000..f88bcb57a27d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 The Android Open 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.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.FixedSizeEdgeDetector +import com.android.compose.animation.scene.SwipeSource +import com.android.compose.animation.scene.SwipeSourceDetector + +/** + * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into + * top-left and top-right. + */ +enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) : + SwipeSource { + TopLeft(resolveEdge = { Resolved.TopLeft }), + TopRight(resolveEdge = { Resolved.TopRight }), + TopStart( + resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight } + ), + TopEnd( + resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft } + ), + Bottom(resolveEdge = { Resolved.Bottom }), + Left(resolveEdge = { Resolved.Left }), + Right(resolveEdge = { Resolved.Right }), + Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }), + End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left }); + + override fun resolve(layoutDirection: LayoutDirection): Resolved { + return resolveEdge(layoutDirection) + } + + enum class Resolved : SwipeSource.Resolved { + TopLeft, + TopRight, + Bottom, + Left, + Right, + } +} + +/** + * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the + * top edge is split in two: top-left and top-right. The split point between the two is dynamic and + * may change during runtime. + * + * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) + * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These + * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and + * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and + * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and + * [SceneContainerEdge.Resolved.Right]. + * + * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e., + * percentage) of screen width to consider the split point between "top-left" and "top-right" + * edges. It is called on each source detection event. + * @param edgeSize The fixed size of each edge. + */ +class SplitEdgeDetector( + val topEdgeSplitFraction: () -> Float, + val edgeSize: Dp, +) : SwipeSourceDetector { + + private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) + + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SceneContainerEdge.Resolved? { + val fixedEdge = + fixedEdgeDetector.source( + layoutSize, + position, + density, + orientation, + ) + return when (fixedEdge) { + Edge.Resolved.Top -> { + val topEdgeSplitFraction = topEdgeSplitFraction() + require(topEdgeSplitFraction in 0f..1f) { + "topEdgeSplitFraction must return a value between 0.0 and 1.0" + } + val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction + if (isLeftSide) SceneContainerEdge.Resolved.TopLeft + else SceneContainerEdge.Resolved.TopRight + } + Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left + Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom + Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right + null -> null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java index a77375c14f26..f69b0cb630d3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java @@ -19,7 +19,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; -import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -133,7 +132,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler private final MessageContainerController mMessageContainerController; private final AnnouncementResolver mAnnouncementResolver; private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; private boolean mAttachRequested; private boolean mDetachRequested; @@ -393,10 +391,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler // Any cleanup needed when the service is being destroyed. @Override public void onDestroy() { - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } removeWindow(); releaseMediaPlayer(); releaseContext(); @@ -598,36 +592,12 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler // Play the shutter sound to notify that we've taken a screenshot playCameraSoundIfNeeded(); - if (screenshotSaveImageExporter()) { - saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { - if (result.uri != null) { - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }); - } else { - saveScreenshotInWorkerThread( - screenshot.getUserHandle(), - /* onComplete */ finisher, - /* actionsReadyListener */ imageData -> { - if (DEBUG_CALLBACK) { - Log.d(TAG, - "returning URI to finisher (Consumer<URI>): " + imageData.uri); - } - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, - mPackageName); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }, - null); - } + saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { + if (result.uri != null) { + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }); } /** @@ -700,35 +670,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler } /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - UserHandle owner, - @NonNull Consumer<Uri> finisher, - @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener, - @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener - quickShareActionsReadyListener) { - SaveImageInBackgroundTask.SaveImageInBackgroundData - data = new SaveImageInBackgroundTask.SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; - data.owner = owner; - data.displayId = mDisplay.getDisplayId(); - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, - mScreenshotSmartActions, data, - mScreenshotNotificationSmartActionsProvider); - mSaveInBgTask.execute(); - } - - /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ private void logScreenshotResultStatus(Uri uri, UserHandle owner) { @@ -745,13 +686,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler } } - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) { - logScreenshotResultStatus(imageData.uri, imageData.owner); - } - private boolean isUserSetupComplete(UserHandle owner) { return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java deleted file mode 100644 index 9bc3bd842664..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; -import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE; -import static com.android.systemui.screenshot.LogConfig.logTag; -import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Process; -import android.os.UserHandle; -import android.provider.DeviceConfig; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.systemui.flags.FeatureFlags; - -import com.google.common.util.concurrent.ListenableFuture; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -/** - * An AsyncTask that saves an image to the media store in the background. - */ -class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { - private static final String TAG = logTag(SaveImageInBackgroundTask.class); - - private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s"; - private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; - - /** - * POD used in the AsyncTask which saves an image in the background. - */ - static class SaveImageInBackgroundData { - public Bitmap image; - public Consumer<Uri> finisher; - public ActionsReadyListener mActionsReadyListener; - public QuickShareActionReadyListener mQuickShareActionsReadyListener; - public UserHandle owner; - public int displayId; - - void clearImage() { - image = null; - } - } - - /** - * Structure returned by the SaveImageInBackgroundTask - */ - public static class SavedImageData { - public Uri uri; - public List<Notification.Action> smartActions; - public Notification.Action quickShareAction; - public UserHandle owner; - public String subject; // Title for sharing - public Long imageTime; // Time at which screenshot was saved - - /** - * Used to reset the return data on error - */ - public void reset() { - uri = null; - smartActions = null; - quickShareAction = null; - subject = null; - imageTime = null; - } - } - - /** - * Structure returned by the QueryQuickShareInBackgroundTask - */ - static class QuickShareData { - public Notification.Action quickShareAction; - - /** - * Used to reset the return data on error - */ - public void reset() { - quickShareAction = null; - } - } - - interface ActionsReadyListener { - void onActionsReady(SavedImageData imageData); - } - - interface QuickShareActionReadyListener { - void onActionsReady(QuickShareData quickShareData); - } - - private final Context mContext; - private FeatureFlags mFlags; - private final ScreenshotSmartActions mScreenshotSmartActions; - private final SaveImageInBackgroundData mParams; - private final SavedImageData mImageData; - private final QuickShareData mQuickShareData; - - private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; - private String mScreenshotId; - private final Random mRandom = new Random(); - private final ImageExporter mImageExporter; - private long mImageTime; - - SaveImageInBackgroundTask( - Context context, - FeatureFlags flags, - ImageExporter exporter, - ScreenshotSmartActions screenshotSmartActions, - SaveImageInBackgroundData data, - ScreenshotNotificationSmartActionsProvider - screenshotNotificationSmartActionsProvider - ) { - mContext = context; - mFlags = flags; - mScreenshotSmartActions = screenshotSmartActions; - mImageData = new SavedImageData(); - mQuickShareData = new QuickShareData(); - mImageExporter = exporter; - - // Prepare all the output metadata - mParams = data; - - // Initialize screenshot notification smart actions provider. - mSmartActionsProvider = screenshotNotificationSmartActionsProvider; - } - - @Override - protected Void doInBackground(Void... paramsUnused) { - if (isCancelled()) { - if (DEBUG_STORAGE) { - Log.d(TAG, "cancelled! returning null"); - } - return null; - } - // TODO: move to constructor / from ScreenshotRequest - final UUID requestId = UUID.randomUUID(); - - Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - - Bitmap image = mParams.image; - mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId); - - boolean savingToOtherUser = mParams.owner != Process.myUserHandle(); - // Smart actions don't yet work for cross-user saves. - boolean smartActionsEnabled = !savingToOtherUser - && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, - true); - try { - if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { - // Since Quick Share target recommendation does not rely on image URL, it is - // queried and surfaced before image compress/export. Action intent would not be - // used, because it does not contain image URL. - Notification.Action quickShare = - queryQuickShareAction(mScreenshotId, image, mParams.owner, null); - if (quickShare != null) { - mQuickShareData.quickShareAction = quickShare; - mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); - } - } - - // Call synchronously here since already on a background thread. - ListenableFuture<ImageExporter.Result> future = - mImageExporter.export(Runnable::run, requestId, image, mParams.owner, - mParams.displayId); - ImageExporter.Result result = future.get(); - Log.d(TAG, "Saved screenshot: " + result); - final Uri uri = result.uri; - mImageTime = result.timestamp; - - CompletableFuture<List<Notification.Action>> smartActionsFuture = - mScreenshotSmartActions.getSmartActionsFuture( - mScreenshotId, uri, image, mSmartActionsProvider, - ScreenshotSmartActionType.REGULAR_SMART_ACTIONS, - smartActionsEnabled, mParams.owner); - List<Notification.Action> smartActions = new ArrayList<>(); - if (smartActionsEnabled) { - int timeoutMs = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, - 1000); - smartActions.addAll(buildSmartActions( - mScreenshotSmartActions.getSmartActions( - mScreenshotId, smartActionsFuture, timeoutMs, - mSmartActionsProvider, - ScreenshotSmartActionType.REGULAR_SMART_ACTIONS), - mContext)); - } - - mImageData.uri = uri; - mImageData.owner = mParams.owner; - mImageData.smartActions = smartActions; - mImageData.quickShareAction = createQuickShareAction( - mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image, - mParams.owner); - mImageData.subject = getSubjectString(mImageTime); - mImageData.imageTime = mImageTime; - - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " - + "finisher.accept(\"" + mImageData.uri + "\""); - } - mParams.finisher.accept(mImageData.uri); - mParams.image = null; - } catch (Exception e) { - // IOException/UnsupportedOperationException may be thrown if external storage is - // not mounted - Log.d(TAG, "Failed to store screenshot", e); - mParams.clearImage(); - mImageData.reset(); - mQuickShareData.reset(); - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)"); - } - mParams.finisher.accept(null); - } - - return null; - } - - /** - * Update the listener run when the saving task completes. Used to avoid showing UI for the - * first screenshot when a second one is taken. - */ - void setActionsReadyListener(ActionsReadyListener listener) { - mParams.mActionsReadyListener = listener; - } - - @Override - protected void onCancelled(Void params) { - // If we are cancelled while the task is running in the background, we may get null - // params. The finisher is expected to always be called back, so just use the baked-in - // params from the ctor in any case. - mImageData.reset(); - mQuickShareData.reset(); - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)"); - } - mParams.finisher.accept(null); - mParams.clearImage(); - } - - private List<Notification.Action> buildSmartActions( - List<Notification.Action> actions, Context context) { - List<Notification.Action> broadcastActions = new ArrayList<>(); - for (Notification.Action action : actions) { - // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions. - Bundle extras = action.getExtras(); - String actionType = extras.getString( - ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, - ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - Intent intent = new Intent(context, SmartActionsReceiver.class) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, action.actionIntent) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); - PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, - mRandom.nextInt(), - intent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title, - broadcastIntent).setContextual(true).addExtras(extras).build()); - } - return broadcastActions; - } - - private static void addIntentExtras(String screenshotId, Intent intent, String actionType, - boolean smartActionsEnabled) { - intent - .putExtra(SmartActionsReceiver.EXTRA_ACTION_TYPE, actionType) - .putExtra(SmartActionsReceiver.EXTRA_ID, screenshotId) - .putExtra(SmartActionsReceiver.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); - } - - /** - * Wrap the quickshare intent and populate the fillin intent with the URI - */ - @VisibleForTesting - Notification.Action createQuickShareAction( - Notification.Action quickShare, String screenshotId, Uri uri, long imageTime, - Bitmap image, UserHandle user) { - if (quickShare == null) { - return null; - } else if (quickShare.actionIntent.isImmutable()) { - Notification.Action quickShareWithUri = - queryQuickShareAction(screenshotId, image, user, uri); - if (quickShareWithUri == null - || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) { - return null; - } - quickShare = quickShareWithUri; - } - - Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, quickShare.actionIntent) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT_FILLIN, - createFillInIntent(uri, imageTime)) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - Bundle extras = quickShare.getExtras(); - String actionType = extras.getString( - ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, - ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - // We only query for quick share actions when smart actions are enabled, so we can assert - // that it's true here. - addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */); - PendingIntent broadcastIntent = - PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title, - broadcastIntent) - .setContextual(true) - .addExtras(extras) - .build(); - } - - private Intent createFillInIntent(Uri uri, long imageTime) { - Intent fillIn = new Intent(); - fillIn.setType("image/png"); - fillIn.putExtra(Intent.EXTRA_STREAM, uri); - fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime)); - // Include URI in ClipData also, so that grantPermission picks it up. - // We don't use setData here because some apps interpret this as "to:". - ClipData clipData = new ClipData( - new ClipDescription("content", new String[]{"image/png"}), - new ClipData.Item(uri)); - fillIn.setClipData(clipData); - fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - return fillIn; - } - - /** - * Query and surface Quick Share chip if it is available. Action intent would not be used, - * because it does not contain image URL which would be populated in {@link - * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)} - */ - - @VisibleForTesting - Notification.Action queryQuickShareAction( - String screenshotId, Bitmap image, UserHandle user, Uri uri) { - CompletableFuture<List<Notification.Action>> quickShareActionsFuture = - mScreenshotSmartActions.getSmartActionsFuture( - screenshotId, uri, image, mSmartActionsProvider, - ScreenshotSmartActionType.QUICK_SHARE_ACTION, - true /* smartActionsEnabled */, user); - int timeoutMs = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS, - 500); - List<Notification.Action> quickShareActions = - mScreenshotSmartActions.getSmartActions( - screenshotId, quickShareActionsFuture, timeoutMs, - mSmartActionsProvider, - ScreenshotSmartActionType.QUICK_SHARE_ACTION); - if (!quickShareActions.isEmpty()) { - return quickShareActions.get(0); - } - return null; - } - - private static String getSubjectString(long imageTime) { - String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime)); - return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 7b802a2a40aa..fe58bc9f34a9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -18,7 +18,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -124,7 +123,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private final MessageContainerController mMessageContainerController; private final AnnouncementResolver mAnnouncementResolver; private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; @@ -373,10 +371,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // Any cleanup needed when the service is being destroyed. @Override public void onDestroy() { - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } removeWindow(); releaseMediaPlayer(); releaseContext(); @@ -525,36 +519,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // Play the shutter sound to notify that we've taken a screenshot playCameraSoundIfNeeded(); - if (screenshotSaveImageExporter()) { - saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { - if (result.uri != null) { - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }); - } else { - saveScreenshotInWorkerThread( - screenshot.getUserHandle(), - /* onComplete */ finisher, - /* actionsReadyListener */ imageData -> { - if (DEBUG_CALLBACK) { - Log.d(TAG, - "returning URI to finisher (Consumer<URI>): " + imageData.uri); - } - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, - mPackageName); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }, - null); - } + saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { + if (result.uri != null) { + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }); } /** @@ -627,35 +597,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - UserHandle owner, - @NonNull Consumer<Uri> finisher, - @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener, - @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener - quickShareActionsReadyListener) { - SaveImageInBackgroundTask.SaveImageInBackgroundData - data = new SaveImageInBackgroundTask.SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; - data.owner = owner; - data.displayId = mDisplay.getDisplayId(); - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, - mScreenshotSmartActions, data, - mScreenshotNotificationSmartActionsProvider); - mSaveInBgTask.execute(); - } - - /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ private void logScreenshotResultStatus(Uri uri, UserHandle owner) { @@ -672,13 +613,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } } - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) { - logScreenshotResultStatus(imageData.uri, imageData.owner); - } - private boolean isUserSetupComplete(UserHandle owner) { return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index ed590c37c384..553d1f51a198 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -103,11 +103,8 @@ internal constructor( override val userContentResolver: ContentResolver get() = userContext.contentResolver - override val userInfo: UserInfo - get() { - val user = userId - return userProfiles.first { it.id == user } - } + override var userInfo: UserInfo by SynchronizedDelegate(UserInfo(context.userId, "", 0)) + protected set /** * Returns a [List<UserInfo>] of all profiles associated with the current user. @@ -187,6 +184,7 @@ internal constructor( userHandle = handle userContext = ctx userProfiles = profiles.map { UserInfo(it) } + userInfo = profiles.first { it.id == user } } return ctx to profiles } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 830649be2a98..4ed4af647fdf 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -42,6 +42,7 @@ import android.util.Log; import android.util.MathUtils; import android.view.MotionEvent; import android.view.VelocityTracker; +import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.WindowInsets; @@ -463,6 +464,9 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mJavaAdapter.alwaysCollectFlow( mCommunalTransitionViewModelLazy.get().isUmoOnCommunal(), this::setShouldUpdateSquishinessOnMedia); + mJavaAdapter.alwaysCollectFlow( + mShadeInteractor.isAnyExpanded(), + this::onAnyExpandedChanged); } private void initNotificationStackScrollLayoutController() { @@ -482,6 +486,10 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum } } + private void onAnyExpandedChanged(boolean isAnyExpanded) { + mQsFrame.setVisibility(isAnyExpanded ? View.VISIBLE : View.INVISIBLE); + } + private void onNotificationScrolled(int newScrollPosition) { updateExpansionEnabledAmbient(); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt index 7425807b716d..99ff94605c39 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt @@ -28,6 +28,8 @@ import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractorEmptyImpl import dagger.Binds import dagger.Module @@ -75,4 +77,8 @@ abstract class ShadeEmptyImplModule { @Binds @SysUISingleton abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository + + @Binds + @SysUISingleton + abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorEmptyImpl): ShadeModeInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index da2024b4ef18..2348a110eb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -41,6 +41,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl import dagger.Binds import dagger.Module import dagger.Provides @@ -54,7 +56,7 @@ abstract class ShadeModule { @SysUISingleton fun provideBaseShadeInteractor( sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>, - sceneContainerOff: Provider<ShadeInteractorLegacyImpl> + sceneContainerOff: Provider<ShadeInteractorLegacyImpl>, ): BaseShadeInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -67,7 +69,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeController( sceneContainerOn: Provider<ShadeControllerSceneImpl>, - sceneContainerOff: Provider<ShadeControllerImpl> + sceneContainerOff: Provider<ShadeControllerImpl>, ): ShadeController { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -80,7 +82,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeAnimationInteractor( sceneContainerOn: Provider<ShadeAnimationInteractorSceneContainerImpl>, - sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl> + sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl>, ): ShadeAnimationInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -93,7 +95,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeBackActionInteractor( sceneContainerOn: Provider<ShadeBackActionInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): ShadeBackActionInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -106,7 +108,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeLockscreenInteractor( sceneContainerOn: Provider<ShadeLockscreenInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): ShadeLockscreenInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -119,7 +121,7 @@ abstract class ShadeModule { @SysUISingleton fun providePanelExpansionInteractor( sceneContainerOn: Provider<PanelExpansionInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): PanelExpansionInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -170,4 +172,8 @@ abstract class ShadeModule { @Binds @SysUISingleton abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository + + @Binds + @SysUISingleton + abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 73e86a2be4aa..6fb96da2c186 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.domain.interactor +import androidx.annotation.FloatRange import com.android.systemui.shade.shared.model.ShadeMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -69,6 +70,20 @@ interface ShadeInteractor : BaseShadeInteractor { * wide as the entire screen. */ val isShadeLayoutWide: StateFlow<Boolean> + + /** + * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold + * between "top-left" and "top-right" for the purposes of dual-shade invocation. + * + * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On + * wide layouts however, a larger fraction is returned because only the area of the system + * status icons is considered top-right. + * + * Note that this fraction only determines the split between the absolute left and right + * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" + * will resolve to "top-left". + */ + @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ @@ -130,7 +145,7 @@ interface BaseShadeInteractor { fun createAnyExpansionFlow( scope: CoroutineScope, shadeExpansion: Flow<Float>, - qsExpansion: Flow<Float> + qsExpansion: Flow<Float>, ): StateFlow<Float> { return combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) } .stateIn(scope, SharingStarted.Eagerly, 0f) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index d51fd28d5458..6c0b55a5dd57 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -47,4 +47,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean + + override fun getTopEdgeSplitFraction(): Float = 0.5f } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 3552092d24e7..3eab02ad30d5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -24,9 +24,6 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository @@ -54,11 +51,14 @@ constructor( keyguardRepository: KeyguardRepository, keyguardTransitionInteractor: KeyguardTransitionInteractor, powerInteractor: PowerInteractor, - private val shadeRepository: ShadeRepository, userSetupRepository: UserSetupRepository, userSwitcherInteractor: UserSwitcherInteractor, private val baseShadeInteractor: BaseShadeInteractor, -) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { + shadeModeInteractor: ShadeModeInteractor, +) : + ShadeInteractor, + BaseShadeInteractor by baseShadeInteractor, + ShadeModeInteractor by shadeModeInteractor { override val isShadeEnabled: StateFlow<Boolean> = disableFlagsRepository.disableFlags .map { it.isShadeEnabled() } @@ -102,17 +102,6 @@ constructor( } } - override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide - - override val shadeMode: StateFlow<ShadeMode> = - isShadeLayoutWide - .map(this::determineShadeMode) - .stateIn( - scope, - SharingStarted.Eagerly, - initialValue = determineShadeMode(isShadeLayoutWide.value) - ) - override val isExpandToQsEnabled: Flow<Boolean> = combine( disableFlagsRepository.disableFlags, @@ -129,12 +118,4 @@ constructor( disableFlags.isQuickSettingsEnabled() && !isDozing } - - private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { - return when { - DualShade.isEnabled -> ShadeMode.Dual - isShadeLayoutWide -> ShadeMode.Split - else -> ShadeMode.Single - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt new file mode 100644 index 000000000000..77ae679bf018 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open 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.shade.domain.interactor + +import androidx.annotation.FloatRange +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * Defines interface for classes that can provide state and business logic related to the mode of + * the shade. + */ +interface ShadeModeInteractor { + + /** + * The version of the shade layout to use. + * + * Note: Most likely, you want to read [isShadeLayoutWide] instead of this. + */ + val shadeMode: StateFlow<ShadeMode> + + /** + * Whether the shade layout should be wide (true) or narrow (false). + * + * In a wide layout, notifications and quick settings each take up only half the screen width + * (whether they are shown at the same time or not). In a narrow layout, they can each be as + * wide as the entire screen. + */ + val isShadeLayoutWide: StateFlow<Boolean> + + /** + * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold + * between "top-left" and "top-right" for the purposes of dual-shade invocation. + * + * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On + * wide layouts however, a larger fraction is returned because only the area of the system + * status icons is considered top-right. + * + * Note that this fraction only determines the split between the absolute left and right + * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" + * will resolve to "top-left". + */ + @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float +} + +class ShadeModeInteractorImpl +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val repository: ShadeRepository, +) : ShadeModeInteractor { + + override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide + + override val shadeMode: StateFlow<ShadeMode> = + isShadeLayoutWide + .map(this::determineShadeMode) + .stateIn( + applicationScope, + SharingStarted.Eagerly, + initialValue = determineShadeMode(isShadeLayoutWide.value), + ) + + @FloatRange(from = 0.0, to = 1.0) + override fun getTopEdgeSplitFraction(): Float { + // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This + // assumption allows us to query its value on demand (during swipe source detection) instead + // of running another infinite coroutine. + // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based + // on the position of system-status icons in the status bar. + return if (repository.isShadeLayoutWide.value) 0.8f else 0.5f + } + + private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { + return when { + DualShade.isEnabled -> ShadeMode.Dual + isShadeLayoutWide -> ShadeMode.Split + else -> ShadeMode.Single + } + } +} + +class ShadeModeInteractorEmptyImpl @Inject constructor() : ShadeModeInteractor { + + override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) + + override val isShadeLayoutWide: StateFlow<Boolean> = MutableStateFlow(false) + + override fun getTopEdgeSplitFraction(): Float = 0.5f +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java index 64dfc6c9eb3c..735b4c34f86f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java @@ -54,6 +54,8 @@ public class VibratorHelper { VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); + private static final VibrationAttributes COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_COMMUNICATION_REQUEST); private final Executor mExecutor; @@ -151,7 +153,7 @@ public class VibratorHelper { vibrate(Process.myUid(), "com.android.systemui", BIOMETRIC_SUCCESS_VIBRATION_EFFECT, reason, - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); + COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES); } /** @@ -160,7 +162,7 @@ public class VibratorHelper { public void vibrateAuthError(String reason) { vibrate(Process.myUid(), "com.android.systemui", BIOMETRIC_ERROR_VIBRATION_EFFECT, reason, - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); + COMMUNICATION_REQUEST_VIBRATION_ATTRIBUTES); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt index af21e75da37e..d36412cf193e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt @@ -16,17 +16,23 @@ package com.android.systemui.statusbar.notification.data +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.SecureSettingsRepositoryModule import com.android.systemui.settings.SystemSettingsRepositoryModule import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger import dagger.Module import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @Module(includes = [SecureSettingsRepositoryModule::class, SystemSettingsRepositoryModule::class]) object NotificationSettingsRepositoryModule { @@ -42,6 +48,19 @@ object NotificationSettingsRepositoryModule { backgroundScope, backgroundDispatcher, secureSettingsRepository, - systemSettingsRepository - ) + systemSettingsRepository) + + @Provides + @IntoMap + @ClassKey(NotificationSettingsRepository::class) + @SysUISingleton + fun provideCoreStartable( + @Application applicationScope: CoroutineScope, + repository: NotificationSettingsRepository, + logger: VisualInterruptionDecisionLogger + ) = CoreStartable { + applicationScope.launch { + repository.isCooldownEnabled.collect { value -> logger.logCooldownSetting(value) } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt index b83259dce298..38cab820c133 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt @@ -102,6 +102,15 @@ constructor(@NotificationInterruptLog val buffer: LogBuffer) { { "AvalancheSuppressor: $str1" } ) } + + fun logCooldownSetting(isEnabled: Boolean) { + buffer.log( + TAG, + INFO, + { bool1 = isEnabled }, + { "Cooldown enabled: $bool1" } + ) + } } private const val TAG = "VisualInterruptionDecisionProvider" 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 8ff1ab640442..1214440a6b65 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 @@ -5404,7 +5404,9 @@ public class NotificationStackScrollLayout println(pw, "intrinsicContentHeight", mIntrinsicContentHeight); println(pw, "contentHeight", mContentHeight); println(pw, "intrinsicPadding", mIntrinsicPadding); - println(pw, "topPadding", getTopPadding()); + if (!SceneContainerFlag.isEnabled()) { + println(pw, "topPadding", getTopPadding()); + } println(pw, "bottomPadding", mBottomPadding); dumpRoundedRectClipping(pw); println(pw, "requestedClipBounds", mRequestedClipBounds); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java index 580431a13d1b..969ff1b4ffe7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java @@ -68,6 +68,7 @@ public class SectionHeaderView extends StackScrollerDecorView { if (mLabelTextId != null) { mLabelView.setText(mLabelTextId); } + mLabelView.setAccessibilityHeading(true); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index ef1bcfc45879..cccac4b479dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -682,7 +682,10 @@ public class StackScrollAlgorithm { // doesn't get updated quickly enough and can cause the footer to flash when // closing the shade. As such, we temporarily also check the ambientState directly. if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { - viewState.hidden = true; + // Note: This is no longer necessary in flexiglass. + if (!SceneContainerFlag.isEnabled()) { + viewState.hidden = true; + } } else { final float footerEnd = algorithmState.mCurrentExpandedYPosition + view.getIntrinsicHeight(); @@ -691,7 +694,6 @@ public class StackScrollAlgorithm { noSpaceForFooter || (ambientState.isClearAllInProgress() && !hasNonClearableNotifs(algorithmState)); } - } else { final boolean shadeClosed = !ambientState.isShadeExpanded(); final boolean isShelfShowing = algorithmState.firstViewInShelf != null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index d770b2003f3b..dc9615c25ada 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -188,15 +188,26 @@ constructor( .startHistoryIntent(view, /* showHistory= */ true) }, ) - launch { - viewModel.shouldIncludeFooterView.collect { animatedVisibility -> - footerView.setVisible( - /* visible = */ animatedVisibility.value, - /* animate = */ animatedVisibility.isAnimating, - ) + if (SceneContainerFlag.isEnabled) { + launch { + viewModel.shouldShowFooterView.collect { animatedVisibility -> + footerView.setVisible( + /* visible = */ animatedVisibility.value, + /* animate = */ animatedVisibility.isAnimating, + ) + } + } + } else { + launch { + viewModel.shouldIncludeFooterView.collect { animatedVisibility -> + footerView.setVisible( + /* visible = */ animatedVisibility.value, + /* animate = */ animatedVisibility.isAnimating, + ) + } } + launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } } } - launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } } disposableHandle.awaitCancellationThenDispose() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index e55492e67d02..4e2a46d78a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Notif import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent import com.android.systemui.util.ui.AnimatedValue @@ -120,6 +121,7 @@ constructor( * This essentially corresponds to having the view set to INVISIBLE. */ val shouldHideFooterView: Flow<Boolean> by lazy { + SceneContainerFlag.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(false) } else { @@ -143,6 +145,7 @@ constructor( * be hidden by another condition (see [shouldHideFooterView] above). */ val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy { + SceneContainerFlag.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(AnimatedValue.NotAnimating(false)) } else { @@ -207,6 +210,76 @@ constructor( } } + // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass. + val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + flowOf(AnimatedValue.NotAnimating(false)) + } else { + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + userSetupInteractor.isUserSetUp, + notificationStackInteractor.isShowingOnLockscreen, + shadeInteractor.isQsFullscreen, + remoteInputInteractor.isRemoteInputActive, + shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(), + ) { + hasNotifications, + isUserSetUp, + isShowingOnLockscreen, + qsFullScreen, + isRemoteInputActive, + shadeLessThanHalfwayExpanded -> + when { + !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer until the user setup is complete, to prevent access + // to settings (b/193149550). + !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Do not show the footer if the lockscreen is visible (incl. AOD), + // except if the shade is opened on top. See also b/219680200. + // Do not animate, as that makes the footer appear briefly when + // transitioning between the shade and keyguard. + isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION + // Do not show the footer if quick settings are fully expanded (except + // for the foldable split shade view). See b/201427195 && b/222699879. + qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer if remote input is active (i.e. user is replying to a + // notification). See b/75984847. + isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // If the shade is not expanded enough, the footer shouldn't be visible. + shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + else -> VisibilityChange.APPEAR_WITH_ANIMATION + } + } + .distinctUntilChanged( + // Equivalent unless visibility changes + areEquivalent = { a: VisibilityChange, b: VisibilityChange -> + a.visible == b.visible + } + ) + // Should we animate the visibility change? + .sample( + // TODO(b/322167853): This check is currently duplicated in FooterViewModel, + // but instead it should be a field in ShadeAnimationInteractor. + combine( + shadeInteractor.isShadeFullyExpanded, + shadeInteractor.isShadeTouchable, + ::Pair + ) + .onStart { emit(Pair(false, false)) } + ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> + // Animate if the shade is interactive, but NOT on the lockscreen. Having + // animations enabled while on the lockscreen makes the footer appear briefly + // when transitioning between the shade and keyguard. + val shouldAnimate = + isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate + AnimatableEvent(visibilityChange.visible, shouldAnimate) + } + .toAnimatedValueFlow() + .dumpWhileCollecting("shouldShowFooterView") + .flowOn(bgDispatcher) + } + } + enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) { DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false), DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 1efad3b9fa66..0e7beb9d17da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -262,6 +262,9 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements releaseAllImmediately(); mReleaseOnExpandFinish = false; } else { + for (NotificationEntry entry: getAllEntries().toList()) { + entry.setSeenInShade(true); + } for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { if (isHeadsUpEntry(entry.getKey())) { // Maybe the heads-up was removed already diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index dd4b0005b034..f3b937100db2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -182,6 +182,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private boolean mBouncerShowingOverDream; private int mAttemptsToShowBouncer = 0; private DelayableExecutor mExecutor; + private boolean mIsSleeping = false; private final PrimaryBouncerExpansionCallback mExpansionCallback = new PrimaryBouncerExpansionCallback() { @@ -713,7 +714,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * {@link #needsFullscreenBouncer()}. */ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) { - if (needsFullscreenBouncer() && !mDozing) { + boolean showBouncer = needsFullscreenBouncer() && !mDozing; + if (Flags.simPinRaceConditionOnRestart()) { + showBouncer = showBouncer && !mIsSleeping; + } + if (showBouncer) { // The keyguard might be showing (already). So we need to hide it. if (!primaryBouncerIsShowing()) { if (SceneContainerFlag.isEnabled()) { @@ -1041,6 +1046,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void onStartedWakingUp() { + mIsSleeping = false; setRootViewAnimationDisabled(false); NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView(); if (navBarView != null) { @@ -1054,6 +1060,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb @Override public void onStartedGoingToSleep() { + mIsSleeping = true; setRootViewAnimationDisabled(true); NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView(); if (navBarView != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt index 85bbe7e53493..d6013192f55e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt @@ -100,7 +100,14 @@ fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? { val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false) val spn = if (statusBarSwitchToSpnFromDataSpn()) { - getStringExtra(EXTRA_SPN) + // Context: b/358669494. Use DATA_SPN if it exists, since that allows carriers to + // customize the display name. Otherwise, fall back to the SPN + val dataSpn = getStringExtra(EXTRA_DATA_SPN) + if (dataSpn.isNullOrEmpty()) { + getStringExtra(EXTRA_SPN) + } else { + dataSpn + } } else { getStringExtra(EXTRA_DATA_SPN) } @@ -109,10 +116,8 @@ fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? { val plmn = getStringExtra(EXTRA_PLMN) val str = StringBuilder() - val strData = StringBuilder() if (showPlmn && plmn != null) { str.append(plmn) - strData.append(plmn) } if (showSpn && spn != null) { if (str.isNotEmpty()) { diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt index 4b0e5d188ffa..6d99183dec33 100644 --- a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -29,11 +29,12 @@ import kotlinx.coroutines.flow.StateFlow class TelephonyInteractor @Inject constructor( - repository: TelephonyRepository, + private val repository: TelephonyRepository, ) { @Annotation.CallState val callState: Flow<Int> = repository.callState val isInCall: StateFlow<Boolean> = repository.isInCall - val hasTelephonyRadio: Boolean = repository.hasTelephonyRadio + val hasTelephonyRadio: Boolean + get() = repository.hasTelephonyRadio } diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 8934d8f8a954..d9e72bf592a0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -1305,13 +1305,12 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, STREAM_UNKNOWN); - final int level = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); final int oldLevel = intent .getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1); if (D.BUG) Log.d(TAG, "onReceive VOLUME_CHANGED_ACTION stream=" + stream - + " level=" + level + " oldLevel=" + oldLevel); + + " oldLevel=" + oldLevel); if (stream != STREAM_UNKNOWN) { - changed = updateStreamLevelW(stream, level); + changed |= onVolumeChangedW(stream, 0); } } else if (action.equals(AudioManager.STREAM_DEVICES_CHANGED_ACTION)) { final int stream = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt index 9715772f089f..28a43df2bfb3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt @@ -16,35 +16,16 @@ package com.android.systemui.volume.dagger -import android.view.accessibility.CaptioningManager import com.android.systemui.accessibility.data.repository.CaptioningRepository import com.android.systemui.accessibility.data.repository.CaptioningRepositoryImpl -import com.android.systemui.accessibility.domain.interactor.CaptioningInteractor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background +import dagger.Binds import dagger.Module -import dagger.Provides -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope @Module interface CaptioningModule { - companion object { - - @Provides - @SysUISingleton - fun provideCaptioningRepository( - captioningManager: CaptioningManager, - @Background coroutineContext: CoroutineContext, - @Application coroutineScope: CoroutineScope, - ): CaptioningRepository = - CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope) - - @Provides - @SysUISingleton - fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor = - CaptioningInteractor(repository) - } + @Binds + @SysUISingleton + fun bindCaptioningRepository(impl: CaptioningRepositoryImpl): CaptioningRepository } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt index 52f2ce63ba21..2e5e389eba9c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn @VolumePanelScope class CaptioningAvailabilityCriteria @@ -45,7 +45,7 @@ constructor( else VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE ) } - .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) override fun isAvailable(): Flow<Boolean> = availability } diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json new file mode 100644 index 000000000000..f37580dd47d4 --- /dev/null +++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json @@ -0,0 +1,831 @@ +{ + "frame_ids": [ + "before", + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272, + 288, + 304, + 320, + 336, + 352, + 368, + 384, + 400, + 416, + 432, + 448, + 464, + 480, + 496, + 512, + 528, + 544, + 560, + 576, + 592, + 608, + 624, + 640, + 656, + 672, + 688, + 704, + 720, + 736, + 752, + 768, + 784, + 800, + 816, + 832, + 848, + 864, + 880, + 896, + 912, + 928, + 944, + 960, + 976, + 992, + 1008, + 1024, + "after" + ], + "features": [ + { + "name": "content_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9954499, + 0.9805035, + 0.9527822, + 0.9092045, + 0.84588075, + 0.7583043, + 0.6424476, + 0.49766344, + 0.33080608, + 0.15650165, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + { + "type": "not_found" + } + ] + }, + { + "name": "content_scale", + "type": "scale", + "data_points": [ + "default", + { + "x": 0.9995097, + "y": 0.9995097, + "pivot": "unspecified" + }, + { + "x": 0.997352, + "y": 0.997352, + "pivot": "unspecified" + }, + { + "x": 0.990635, + "y": 0.990635, + "pivot": "unspecified" + }, + { + "x": 0.97249764, + "y": 0.97249764, + "pivot": "unspecified" + }, + { + "x": 0.94287145, + "y": 0.94287145, + "pivot": "unspecified" + }, + { + "x": 0.9128026, + "y": 0.9128026, + "pivot": "unspecified" + }, + { + "x": 0.8859569, + "y": 0.8859569, + "pivot": "unspecified" + }, + { + "x": 0.8629254, + "y": 0.8629254, + "pivot": "unspecified" + }, + { + "x": 0.8442908, + "y": 0.8442908, + "pivot": "unspecified" + }, + { + "x": 0.8303209, + "y": 0.8303209, + "pivot": "unspecified" + }, + { + "x": 0.8205137, + "y": 0.8205137, + "pivot": "unspecified" + }, + { + "x": 0.81387186, + "y": 0.81387186, + "pivot": "unspecified" + }, + { + "x": 0.80941653, + "y": 0.80941653, + "pivot": "unspecified" + }, + { + "x": 0.80641484, + "y": 0.80641484, + "pivot": "unspecified" + }, + { + "x": 0.80437464, + "y": 0.80437464, + "pivot": "unspecified" + }, + { + "x": 0.80297637, + "y": 0.80297637, + "pivot": "unspecified" + }, + { + "x": 0.80201286, + "y": 0.80201286, + "pivot": "unspecified" + }, + { + "x": 0.8013477, + "y": 0.8013477, + "pivot": "unspecified" + }, + { + "x": 0.8008894, + "y": 0.8008894, + "pivot": "unspecified" + }, + { + "x": 0.8005756, + "y": 0.8005756, + "pivot": "unspecified" + }, + { + "x": 0.80036324, + "y": 0.80036324, + "pivot": "unspecified" + }, + { + "x": 0.8002219, + "y": 0.8002219, + "pivot": "unspecified" + }, + { + "x": 0.80012995, + "y": 0.80012995, + "pivot": "unspecified" + }, + { + "x": 0.8000721, + "y": 0.8000721, + "pivot": "unspecified" + }, + { + "x": 0.80003715, + "y": 0.80003715, + "pivot": "unspecified" + }, + { + "x": 0.8000173, + "y": 0.8000173, + "pivot": "unspecified" + }, + { + "x": 0.800007, + "y": 0.800007, + "pivot": "unspecified" + }, + { + "x": 0.8000022, + "y": 0.8000022, + "pivot": "unspecified" + }, + { + "x": 0.8000004, + "y": 0.8000004, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.79999995, + "y": 0.79999995, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "x": 0.8, + "y": 0.8, + "pivot": "unspecified" + }, + { + "type": "not_found" + } + ] + }, + { + "name": "content_offset", + "type": "dpOffset", + "data_points": [ + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0 + }, + { + "x": 0, + "y": 0.5714286 + }, + { + "x": 0, + "y": 2.857143 + }, + { + "x": 0, + "y": 7.142857 + }, + { + "x": 0, + "y": 13.714286 + }, + { + "x": 0, + "y": 23.142857 + }, + { + "x": 0, + "y": 36.285713 + }, + { + "x": 0, + "y": 53.714287 + }, + { + "x": 0, + "y": 75.42857 + }, + { + "x": 0, + "y": 100.28571 + }, + { + "x": 0, + "y": 126.57143 + }, + { + "x": 0, + "y": 151.42857 + }, + { + "x": 0, + "y": 174 + }, + { + "x": 0, + "y": 193.42857 + }, + { + "x": 0, + "y": 210.28572 + }, + { + "x": 0, + "y": 224.85715 + }, + { + "x": 0, + "y": 237.14285 + }, + { + "x": 0, + "y": 247.71428 + }, + { + "x": 0, + "y": 256.85715 + }, + { + "x": 0, + "y": 264.57144 + }, + { + "x": 0, + "y": 271.42856 + }, + { + "x": 0, + "y": 277.14285 + }, + { + "x": 0, + "y": 282 + }, + { + "x": 0, + "y": 286.2857 + }, + { + "x": 0, + "y": 289.7143 + }, + { + "x": 0, + "y": 292.57144 + }, + { + "x": 0, + "y": 294.85715 + }, + { + "x": 0, + "y": 296.85715 + }, + { + "x": 0, + "y": 298.2857 + }, + { + "x": 0, + "y": 299.14285 + }, + { + "x": 0, + "y": 299.7143 + }, + { + "x": 0, + "y": 300 + }, + { + "x": 0, + "y": 0 + }, + { + "type": "not_found" + } + ] + }, + { + "name": "background_alpha", + "type": "float", + "data_points": [ + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0.9900334, + 0.8403853, + 0.71002257, + 0.5979084, + 0.50182605, + 0.41945767, + 0.34874845, + 0.28797746, + 0.23573697, + 0.19087732, + 0.1524564, + 0.11970067, + 0.091962695, + 0.068702936, + 0.049464583, + 0.033859253, + 0.021552086, + 0.012255073, + 0.005717635, + 0.0017191172, + 6.711483e-05, + 0, + { + "type": "not_found" + } + ] + } + ] +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt index 43a780357027..c42e25b20e0d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/EmergencyButtonControllerTest.kt @@ -29,7 +29,7 @@ import com.android.internal.logging.MetricsLogger import com.android.internal.widget.LockPatternUtils import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.haptics.msdl.FakeMSDLPlayer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.shade.ShadeController import com.android.systemui.statusbar.policy.ConfigurationController @@ -72,7 +72,7 @@ class EmergencyButtonControllerTest : SysuiTestCase() { val mainExecutor = FakeExecutor(fakeSystemClock) val backgroundExecutor = FakeExecutor(fakeSystemClock) private val kosmos = testKosmos() - private val msdlPlayer: FakeMSDLPlayer = kosmos.msdlPlayer + private val msdlPlayer = kosmos.fakeMSDLPlayer lateinit var underTest: EmergencyButtonController diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java index 4a5c1bed7b44..038ec406c3d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java @@ -32,12 +32,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -66,7 +68,7 @@ public class AccessibilityButtonModeObserverTest extends SysuiTestCase { Settings.Secure.ACCESSIBILITY_BUTTON_MODE, Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, MY_USER_ID); mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java index a5a7a4a09227..f5649266d0a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java @@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -62,7 +64,7 @@ public class AccessibilityButtonTargetsObserverTest extends SysuiTestCase { public void setUp() { when(mUserTracker.getUserId()).thenReturn(MY_USER_ID); mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java index ba990efd5162..afed12fb700b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java @@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -62,7 +64,7 @@ public class AccessibilityGestureTargetsObserverTest extends SysuiTestCase { public void setUp() { when(mUserTracker.getUserId()).thenReturn(MY_USER_ID); mAccessibilityGestureTargetsObserver = new AccessibilityGestureTargetsObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java index 9222fc2222be..1d88b904668c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -72,7 +73,7 @@ public class SecureSettingsContentObserverTest extends SysuiTestCase { protected FakeSecureSettingsContentObserver(Context context, UserTracker userTracker, String secureSettingsKey) { - super(context, userTracker, secureSettingsKey); + super(context, userTracker, Mockito.mock(SecureSettings.class), secureSettingsKey); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index d3b7d2207854..662815ee7cbe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -52,7 +52,6 @@ import android.widget.Spinner; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.internal.logging.UiEventLogger; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; @@ -92,6 +91,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); + private static final int TEST_LAUNCH_SOURCE_ID = 1; private static final String DEVICE_ADDRESS = "AA:BB:CC:DD:EE:FF"; private static final String DEVICE_NAME = "test_name"; private static final String TEST_PKG = "pkg"; @@ -124,7 +124,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private AudioManager mAudioManager; @Mock - private UiEventLogger mUiEventLogger; + private HearingDevicesUiEventLogger mUiEventLogger; @Mock private CachedBluetoothDevice mCachedDevice; @Mock @@ -182,7 +182,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS); - verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR); + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, + TEST_LAUNCH_SOURCE_ID); } @Test @@ -196,7 +197,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { anyInt(), any()); assertThat(intentCaptor.getValue().getAction()).isEqualTo( HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS); - verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK); + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, + TEST_LAUNCH_SOURCE_ID); } @Test @@ -207,7 +209,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialogDelegate.onDeviceItemClicked(mHearingDeviceItem, new View(mContext)); verify(mCachedDevice).disconnect(); - verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT); + verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT, + TEST_LAUNCH_SOURCE_ID); } @Test @@ -304,6 +307,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialogDelegate = new HearingDevicesDialogDelegate( mContext, true, + TEST_LAUNCH_SOURCE_ID, mDialogFactory, mActivityStarter, mDialogTransitionAnimator, @@ -327,6 +331,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialogDelegate = new HearingDevicesDialogDelegate( mContext, false, + TEST_LAUNCH_SOURCE_ID, mDialogFactory, mActivityStarter, mDialogTransitionAnimator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 1e2369034bf7..7889b3cd6cc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -61,10 +61,12 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.display.data.repository.FakeDisplayRepository +import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.events.ANIMATING_OUT +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock @@ -145,6 +147,9 @@ open class AuthContainerViewTest : SysuiTestCase() { private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) + private val kosmos = testKosmos() + private val msdlPlayer = kosmos.msdlPlayer + private var authContainer: TestAuthContainerView? = null @Before @@ -668,7 +673,8 @@ open class AuthContainerViewTest : SysuiTestCase() { { credentialViewModel }, fakeExecutor, vibrator, - lazyViewCapture + lazyViewCapture, + msdlPlayer, ) { override fun postOnAnimation(runnable: Runnable) { runnable.run() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 4fc41669b2c9..55fd3440ea07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -37,12 +37,15 @@ import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.Surface import androidx.test.filters.SmallTest import com.android.app.activityTaskManager +import com.android.keyguard.AuthInteractionProperties +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.Utils.toBitmap @@ -72,6 +75,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.util.mockito.withArgCaptor +import com.google.android.msdl.data.model.MSDLToken import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first @@ -124,6 +128,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private val defaultLogoDescriptionFromActivityInfo = "Test Coke App" private val logoDescriptionFromApp = "Test Cake App" private val packageNameForLogoWithOverrides = "should.use.overridden.logo" + private val authInteractionProperties = AuthInteractionProperties() /** Prompt panel size padding */ private val smallHorizontalGuidelinePadding = @@ -707,31 +712,66 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun set_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) - val confirmHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(confirmHaptics?.hapticFeedbackConstant) - .isEqualTo( - if (expectConfirmation) HapticFeedbackConstants.NO_HAPTICS - else HapticFeedbackConstants.BIOMETRIC_CONFIRM - ) - assertThat(confirmHaptics?.flag).isNull() + val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + if (expectConfirmation) { + assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None) + } else { + val confirmHaptics = + hapticsPreConfirm as PromptViewModel.HapticsToPlay.HapticConstant + assertThat(confirmHaptics.constant) + .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + assertThat(confirmHaptics.flag).isNull() + } if (expectConfirmation) { kosmos.promptViewModel.confirmAuthenticated() } - val confirmedHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(confirmedHaptics?.hapticFeedbackConstant) + val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val confirmedHaptics = + hapticsPostConfirm as PromptViewModel.HapticsToPlay.HapticConstant + assertThat(confirmedHaptics.constant) .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) - assertThat(confirmedHaptics?.flag).isNull() + assertThat(confirmedHaptics.flag).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun set_msdl_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() = + runGenericTest { + val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) + + kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) + + val hapticsPreConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + + if (expectConfirmation) { + assertThat(hapticsPreConfirm).isEqualTo(PromptViewModel.HapticsToPlay.None) + } else { + val confirmHaptics = hapticsPreConfirm as PromptViewModel.HapticsToPlay.MSDL + assertThat(confirmHaptics.token).isEqualTo(MSDLToken.UNLOCK) + assertThat(confirmHaptics.properties).isEqualTo(authInteractionProperties) + } + + if (expectConfirmation) { + kosmos.promptViewModel.confirmAuthenticated() + } + + val hapticsPostConfirm by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val confirmedHaptics = hapticsPostConfirm as PromptViewModel.HapticsToPlay.MSDL + assertThat(confirmedHaptics.token).isEqualTo(MSDLToken.UNLOCK) + assertThat(confirmedHaptics.properties).isEqualTo(authInteractionProperties) } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessHaptic_SetsConfirmConstant() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) @@ -740,20 +780,48 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.confirmAuthenticated() } - val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(currentHaptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) - assertThat(currentHaptics?.flag).isNull() + val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant + assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + assertThat(currentHaptics.flag).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playSuccessHaptic_SetsUnlockMSDLFeedback() = runGenericTest { + val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) + kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) + + if (expectConfirmation) { + kosmos.promptViewModel.confirmAuthenticated() + } + + val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL + assertThat(currentHaptics.token).isEqualTo(MSDLToken.UNLOCK) + assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties) } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playErrorHaptic_SetsRejectConstant() = runGenericTest { kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false) - val currentHaptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(currentHaptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) - assertThat(currentHaptics?.flag).isNull() + val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val currentHaptics = haptics as PromptViewModel.HapticsToPlay.HapticConstant + assertThat(currentHaptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(currentHaptics.flag).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playErrorHaptic_SetsFailureMSDLFeedback() = runGenericTest { + kosmos.promptViewModel.showTemporaryError("test", "messageAfterError", false) + + val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val currentHaptics = haptics as PromptViewModel.HapticsToPlay.MSDL + assertThat(currentHaptics.token).isEqualTo(MSDLToken.FAILURE) + assertThat(currentHaptics.properties).isEqualTo(authInteractionProperties) } // biometricprompt_sfps_fingerprint_authenticating reused across rotations @@ -855,6 +923,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun set_haptic_on_errors() = runGenericTest { kosmos.promptViewModel.showTemporaryError( "so sad", @@ -863,13 +932,30 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa hapticFeedback = true, ) - val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(haptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) - assertThat(haptics?.flag).isNull() + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant + assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(haptics.flag).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun set_msdl_haptic_on_errors() = runGenericTest { + kosmos.promptViewModel.showTemporaryError( + "so sad", + messageAfterError = "", + authenticateAfterError = false, + hapticFeedback = true, + ) + + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL + assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE) + assertThat(haptics.properties).isEqualTo(authInteractionProperties) } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun plays_haptic_on_errors_unless_skipped() = runGenericTest { kosmos.promptViewModel.showTemporaryError( "still sad", @@ -878,11 +964,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa hapticFeedback = false, ) - val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) - assertThat(haptics?.hapticFeedbackConstant).isEqualTo(HapticFeedbackConstants.NO_HAPTICS) + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None) + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun plays_msdl_haptic_on_errors_unless_skipped() = runGenericTest { + kosmos.promptViewModel.showTemporaryError( + "still sad", + messageAfterError = "", + authenticateAfterError = false, + hapticFeedback = false, + ) + + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + assertThat(hapticsToPlay).isEqualTo(PromptViewModel.HapticsToPlay.None) } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun plays_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest { val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0) @@ -894,15 +995,37 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa hapticFeedback = true, ) - val haptics by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.HapticConstant if (expectConfirmation) { - assertThat(haptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) - assertThat(haptics?.flag).isNull() + assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_REJECT) + assertThat(haptics.flag).isNull() } else { - assertThat(haptics?.hapticFeedbackConstant) - .isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + assertThat(haptics.constant).isEqualTo(HapticFeedbackConstants.BIOMETRIC_CONFIRM) + } + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun plays_msdl_haptic_on_error_after_auth_when_confirmation_needed() = runGenericTest { + val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) + kosmos.promptViewModel.showAuthenticated(testCase.authenticatedModality, 0) + + kosmos.promptViewModel.showTemporaryError( + "still sad", + messageAfterError = "", + authenticateAfterError = false, + hapticFeedback = true, + ) + + val hapticsToPlay by collectLastValue(kosmos.promptViewModel.hapticsToPlay) + val haptics = hapticsToPlay as PromptViewModel.HapticsToPlay.MSDL + if (expectConfirmation) { + assertThat(haptics.token).isEqualTo(MSDLToken.FAILURE) + } else { + assertThat(haptics.token).isEqualTo(MSDLToken.UNLOCK) } + assertThat(haptics.properties).isEqualTo(authInteractionProperties) } private suspend fun TestScope.showTemporaryErrors( diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt new file mode 100644 index 000000000000..22946c8e6ad0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2024 The Android Open 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.bouncer.ui.composable + +import android.app.AlertDialog +import android.platform.test.annotations.MotionTest +import android.testing.TestableLooper.RunWithLooper +import androidx.activity.BackEventCompat +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.isFinite +import androidx.compose.ui.geometry.isUnspecified +import androidx.compose.ui.semantics.SemanticsNode +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.Scale +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.compose.animation.scene.isElement +import com.android.compose.animation.scene.testing.lastAlphaForTesting +import com.android.compose.animation.scene.testing.lastScaleForTesting +import com.android.compose.theme.PlatformTheme +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.ui.BouncerDialogFactory +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel +import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel +import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.motion.createSysUiComposeMotionTestRule +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.domain.startable.sceneContainerStartable +import com.android.systemui.scene.shared.logger.sceneLogger +import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.sceneDataSourceDelegator +import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.scene.ui.composable.SceneContainer +import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.testKosmos +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.json.JSONObject +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations +import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot +import platform.test.motion.compose.ComposeRecordingSpec +import platform.test.motion.compose.MotionControl +import platform.test.motion.compose.feature +import platform.test.motion.compose.recordMotion +import platform.test.motion.compose.runTest +import platform.test.motion.golden.DataPoint +import platform.test.motion.golden.DataPointType +import platform.test.motion.golden.DataPointTypes +import platform.test.motion.golden.FeatureCapture +import platform.test.motion.golden.UnknownTypeException +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays.Phone + +/** MotionTest for the Bouncer Predictive Back animation */ +@LargeTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +@EnableSceneContainer +@MotionTest +class BouncerPredictiveBackTest : SysuiTestCase() { + + private val deviceSpec = DeviceEmulationSpec(Phone) + private val kosmos = testKosmos() + + @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec) + private val androidComposeTestRule = + motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *> + + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) } + private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer } + private val Kosmos.sceneContainerConfig by Fixture { + val navigationDistances = + mapOf( + Scenes.Lockscreen to 1, + Scenes.Bouncer to 0, + ) + SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances) + } + + private val transitionState by lazy { + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) + ) + } + private val sceneContainerViewModel by lazy { + SceneContainerViewModel( + sceneInteractor = kosmos.sceneInteractor, + falsingInteractor = kosmos.falsingInteractor, + powerInteractor = kosmos.powerInteractor, + shadeInteractor = kosmos.shadeInteractor, + splitEdgeDetector = kosmos.splitEdgeDetector, + logger = kosmos.sceneLogger, + motionEventHandlerReceiver = {}, + ) + .apply { setTransitionState(transitionState) } + } + + private val bouncerDialogFactory = + object : BouncerDialogFactory { + override fun invoke(): AlertDialog { + throw AssertionError() + } + } + private val bouncerSceneActionsViewModelFactory = + object : BouncerUserActionsViewModel.Factory { + override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor) + } + private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel + private val bouncerSceneContentViewModelFactory = + object : BouncerSceneContentViewModel.Factory { + override fun create() = bouncerSceneContentViewModel + } + private val bouncerScene = + BouncerScene( + bouncerSceneActionsViewModelFactory, + bouncerSceneContentViewModelFactory, + bouncerDialogFactory + ) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel + + val startable = kosmos.sceneContainerStartable + startable.start() + } + + @Test + fun bouncerPredictiveBackMotion() = + motionTestRule.runTest { + val motion = + recordMotion( + content = { play -> + PlatformTheme { + BackGestureAnimation(play) + SceneContainer( + viewModel = + rememberViewModel("BouncerPredictiveBackTest") { + sceneContainerViewModel + }, + sceneByKey = + mapOf( + Scenes.Lockscreen to FakeLockscreen(), + Scenes.Bouncer to bouncerScene + ), + initialSceneKey = Scenes.Bouncer, + overlayByKey = emptyMap(), + dataSourceDelegator = kosmos.sceneDataSourceDelegator + ) + } + }, + ComposeRecordingSpec( + MotionControl( + delayRecording = { + awaitCondition { + sceneInteractor.transitionState.value.isTransitioning() + } + } + ) { + awaitCondition { + sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen) + } + } + ) { + feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha") + feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale") + feature( + isElement(Bouncer.Elements.Content), + positionInRoot, + "content_offset" + ) + feature( + isElement(Bouncer.Elements.Background), + elementAlpha, + "background_alpha" + ) + } + ) + + assertThat(motion).timeSeriesMatchesGolden() + } + + @Composable + private fun BackGestureAnimation(play: Boolean) { + val backProgress = remember { Animatable(0f) } + + LaunchedEffect(play) { + if (play) { + val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher + androidComposeTestRule.runOnUiThread { + dispatcher.dispatchOnBackStarted(backEvent()) + } + backProgress.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 500) + ) { + androidComposeTestRule.runOnUiThread { + dispatcher.dispatchOnBackProgressed( + backEvent(progress = backProgress.value) + ) + if (backProgress.value == 1f) { + dispatcher.onBackPressed() + } + } + } + } + } + } + + private fun backEvent(progress: Float = 0f): BackEventCompat { + return BackEventCompat( + touchX = 0f, + touchY = 0f, + progress = progress, + swipeEdge = BackEventCompat.EDGE_LEFT, + ) + } + + private class FakeLockscreen : ExclusiveActivatable(), Scene { + override val key: SceneKey = Scenes.Lockscreen + override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf() + + @Composable + override fun SceneScope.Content(modifier: Modifier) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text(text = "Fake Lockscreen") + } + } + + override suspend fun onActivated() = awaitCancellation() + } + + companion object { + private val elementAlpha = + FeatureCapture<SemanticsNode, Float>("alpha") { + DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float) + } + + private val elementScale = + FeatureCapture<SemanticsNode, Scale>("scale") { + DataPoint.of(it.lastScaleForTesting, scale) + } + + private val scale: DataPointType<Scale> = + DataPointType( + "scale", + jsonToValue = { + when (it) { + "unspecified" -> Scale.Unspecified + "default" -> Scale.Default + "zero" -> Scale.Zero + is JSONObject -> { + val pivot = it.get("pivot") + Scale( + scaleX = it.getDouble("x").toFloat(), + scaleY = it.getDouble("y").toFloat(), + pivot = + when (pivot) { + "unspecified" -> Offset.Unspecified + "infinite" -> Offset.Infinite + is JSONObject -> + Offset( + pivot.getDouble("x").toFloat(), + pivot.getDouble("y").toFloat() + ) + else -> throw UnknownTypeException() + } + ) + } + else -> throw UnknownTypeException() + } + }, + valueToJson = { + when (it) { + Scale.Unspecified -> "unspecified" + Scale.Default -> "default" + Scale.Zero -> "zero" + else -> { + JSONObject().apply { + put("x", it.scaleX) + put("y", it.scaleY) + put( + "pivot", + when { + it.pivot.isUnspecified -> "unspecified" + !it.pivot.isFinite -> "infinite" + else -> + JSONObject().apply { + put("x", it.pivot.x) + put("y", it.pivot.y) + } + } + ) + } + } + } + } + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java index a1bea0620528..637771790b28 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java @@ -104,6 +104,7 @@ public class BrightLineClassifierTest extends SysuiTestCase { when(mFalsingDataProvider.getRecentMotionEvents()).thenReturn(mMotionEventList); when(mKeyguardStateController.isShowing()).thenReturn(true); when(mFalsingDataProvider.isUnfolded()).thenReturn(false); + when(mFalsingDataProvider.isTouchScreenSource()).thenReturn(true); mBrightLineFalsingManager = new BrightLineFalsingManager(mFalsingDataProvider, mMetricsLogger, mClassifiers, mSingleTapClassfier, mLongTapClassifier, mDoubleTapClassifier, mHistoryTracker, mKeyguardStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index 7cc91853a749..bfb8a57e6271 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryBackgroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView @@ -86,6 +87,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { { mock(DeviceEntryBackgroundViewModel::class.java) }, { falsingManager }, { mock(VibratorHelper::class.java) }, + logcatLogBuffer(), ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index ad7a5b62b912..3c743744dd58 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -967,7 +967,8 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.app).isEqualTo(APP_NAME) - assertThat(data.actions).hasSize(1) + // resume button is a semantic action. + assertThat(data.actions).hasSize(0) assertThat(data.semanticActions!!.playOrPause).isNotNull() assertThat(data.lastActive).isAtLeast(currentTime) verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) @@ -994,7 +995,8 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.app).isEqualTo(APP_NAME) - assertThat(data.actions).hasSize(1) + // resume button is a semantic action. + assertThat(data.actions).hasSize(0) assertThat(data.semanticActions!!.playOrPause).isNotNull() assertThat(data.lastActive).isAtLeast(currentTime) assertThat(data.isExplicit).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt index c0f503d7f1cb..4cf7de3d7a63 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -69,6 +69,8 @@ import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.mediaLogger +import com.android.systemui.media.controls.shared.mockMediaLogger import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC import com.android.systemui.media.controls.shared.model.MediaData @@ -140,7 +142,7 @@ private fun <T> anyObject(): T { @RunWith(ParameterizedAndroidJunit4::class) @EnableSceneContainer class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger } private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val settings = kosmos.fakeSettings @@ -257,6 +259,7 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { keyguardUpdateMonitor = keyguardUpdateMonitor, mediaDataRepository = kosmos.mediaDataRepository, mediaDataLoader = { kosmos.mediaDataLoader }, + mediaLogger = kosmos.mediaLogger, ) mediaDataProcessor.start() testScope.runCurrent() @@ -984,7 +987,8 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.app).isEqualTo(APP_NAME) - assertThat(data.actions).hasSize(1) + // resume button is a semantic action. + assertThat(data.actions).hasSize(0) assertThat(data.semanticActions!!.playOrPause).isNotNull() assertThat(data.lastActive).isAtLeast(currentTime) verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) @@ -1011,7 +1015,8 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.app).isEqualTo(APP_NAME) - assertThat(data.actions).hasSize(1) + // resume button is a semantic action. + assertThat(data.actions).hasSize(0) assertThat(data.semanticActions!!.playOrPause).isNotNull() assertThat(data.lastActive).isAtLeast(currentTime) assertThat(data.isExplicit).isTrue() @@ -2476,6 +2481,55 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(mediaDataCaptor.value.artwork).isNull() } + @Test + @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDuplicateNotification_doesNotCallListeners() { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + reset(listener) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + testScope.assertRunAllReady(foreground = 0, background = 1) + verify(listener, never()) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(kosmos.mediaLogger).logDuplicateMediaNotification(eq(KEY)) + } + + @Test + @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION) + fun postDuplicateNotification_callsListeners() { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + + mediaDataProcessor.addInternalListener(mediaDataFilter) + mediaDataFilter.mediaDataProcessor = mediaDataProcessor + addNotificationAndLoad() + reset(listener) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + testScope.assertRunAllReady(foreground = 1, background = 1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY)) + } + private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) { runCurrent() if (Flags.mediaLoadMetadataViaMediaDataLoader()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt index c1bba4d4d60c..680df1584f89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt @@ -72,7 +72,6 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var mediaController: MediaController @Mock private lateinit var logger: MediaTimeoutLogger @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController - private lateinit var executor: FakeExecutor @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Mock private lateinit var sessionCallback: (String) -> Unit @@ -88,6 +87,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() { private lateinit var resumeData: MediaData private lateinit var mediaTimeoutListener: MediaTimeoutListener private var clock = FakeSystemClock() + private lateinit var mainExecutor: FakeExecutor + private lateinit var bgExecutor: FakeExecutor + private lateinit var uiExecutor: FakeExecutor @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var smartspaceData: SmartspaceMediaData @@ -95,11 +97,15 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun setup() { whenever(mediaControllerFactory.create(any())).thenReturn(mediaController) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) - executor = FakeExecutor(clock) + mainExecutor = FakeExecutor(clock) + bgExecutor = FakeExecutor(clock) + uiExecutor = FakeExecutor(clock) mediaTimeoutListener = MediaTimeoutListener( mediaControllerFactory, - executor, + bgExecutor, + uiExecutor, + mainExecutor, logger, statusBarStateController, clock, @@ -143,30 +149,31 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING) whenever(mediaController.playbackState).thenReturn(playingState) - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) verify(logger).logPlaybackState(eq(KEY), eq(playingState)) // Ignores if same key clearInvocations(mediaController) - mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData) + loadMediaData(KEY, KEY, mediaData) verify(mediaController, never()).registerCallback(anyObject()) } @Test fun testOnMediaDataLoaded_registersTimeout_whenPaused() { - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false)) - assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @Test fun testOnMediaDataRemoved_unregistersPlaybackListener() { - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) mediaTimeoutListener.onMediaDataRemoved(KEY, false) + assertThat(bgExecutor.runAllReady()).isEqualTo(1) verify(mediaController).unregisterCallback(anyObject()) // Ignores duplicate requests @@ -178,50 +185,50 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testOnMediaDataRemoved_clearsTimeout() { // GIVEN media that is paused - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) - assertThat(executor.numPending()).isEqualTo(1) + loadMediaData(KEY, null, mediaData) + assertThat(mainExecutor.numPending()).isEqualTo(1) // WHEN the media is removed mediaTimeoutListener.onMediaDataRemoved(KEY, false) // THEN the timeout runnable is cancelled - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) } @Test fun testOnMediaDataLoaded_migratesKeys() { val newKey = "NEWKEY" // From not playing - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) clearInvocations(mediaController) // To playing val playingState = mock(android.media.session.PlaybackState::class.java) whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING) whenever(mediaController.playbackState).thenReturn(playingState) - mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData) + loadMediaData(newKey, KEY, mediaData) verify(mediaController).unregisterCallback(anyObject()) verify(mediaController).registerCallback(anyObject()) verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true)) // Enqueues callback - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) } @Test fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() { val newKey = "NEWKEY" // From not playing - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) clearInvocations(mediaController) // Migrate, still not playing val playingState = mock(android.media.session.PlaybackState::class.java) whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED) whenever(mediaController.playbackState).thenReturn(playingState) - mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData) + loadMediaData(newKey, KEY, mediaData) // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor // is another scheduled - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(logger).logUpdateListener(eq(newKey), eq(false)) } @@ -233,8 +240,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaCallbackCaptor.value.onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() ) - assertThat(executor.numPending()).isEqualTo(1) - assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) + assertThat(mainExecutor.numPending()).isEqualTo(1) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @Test @@ -245,7 +252,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaCallbackCaptor.value.onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build() ) - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) verify(logger).logTimeoutCancelled(eq(KEY), any()) } @@ -257,7 +264,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaCallbackCaptor.value.onPlaybackStateChanged( PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build() ) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) } @Test @@ -265,7 +272,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Assuming we're have a pending timeout testOnPlaybackStateChanged_schedulesTimeout_whenPaused() - with(executor) { + with(mainExecutor) { advanceClockToNext() runAllReady() } @@ -274,7 +281,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testIsTimedOut() { - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse() } @@ -282,16 +289,17 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testOnSessionDestroyed_active_clearsTimeout() { // GIVEN media that is paused val mediaPaused = mediaData.copy(isPlaying = false) - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused) + loadMediaData(KEY, null, mediaPaused) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // WHEN the session is destroyed mediaCallbackCaptor.value.onSessionDestroyed() // THEN the controller is unregistered and timeout run + assertThat(bgExecutor.runAllReady()).isEqualTo(1) verify(mediaController).unregisterCallback(anyObject()) - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) verify(logger).logSessionDestroyed(eq(KEY)) verify(sessionCallback).invoke(eq(KEY)) } @@ -306,11 +314,11 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING) whenever(mediaController.playbackState).thenReturn(playingState) val mediaPlaying = mediaData.copy(isPlaying = true) - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying) + loadMediaData(KEY, null, mediaPlaying) // THEN the timeout runnable will update the state - assertThat(executor.numPending()).isEqualTo(1) - with(executor) { + assertThat(mainExecutor.numPending()).isEqualTo(1) + with(mainExecutor) { advanceClockToNext() runAllReady() } @@ -322,31 +330,32 @@ class MediaTimeoutListenerTest : SysuiTestCase() { fun testOnSessionDestroyed_resume_continuesTimeout() { // GIVEN resume media with session info val resumeWithSession = resumeData.copy(token = session.sessionToken) - mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession) + loadMediaData(PACKAGE, null, resumeWithSession) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // WHEN the session is destroyed mediaCallbackCaptor.value.onSessionDestroyed() // THEN the controller is unregistered, but the timeout is still scheduled + assertThat(bgExecutor.runAllReady()).isEqualTo(1) verify(mediaController).unregisterCallback(anyObject()) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(sessionCallback, never()).invoke(eq(KEY)) } @Test fun testOnMediaDataLoaded_activeToResume_registersTimeout() { // WHEN a regular media is loaded - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(KEY, null, mediaData) // AND it turns into a resume control - mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData) + loadMediaData(PACKAGE, KEY, resumeData) // THEN we register a timeout - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) - assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) } @Test @@ -355,42 +364,42 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val pausedState = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() whenever(mediaController.playbackState).thenReturn(pausedState) - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) - assertThat(executor.numPending()).isEqualTo(1) + loadMediaData(KEY, null, mediaData) + assertThat(mainExecutor.numPending()).isEqualTo(1) // AND it turns into a resume control - mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData) + loadMediaData(PACKAGE, KEY, resumeData) // THEN we update the timeout length - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) - assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) } @Test fun testOnMediaDataLoaded_resumption_registersTimeout() { // WHEN a resume media is loaded - mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) + loadMediaData(PACKAGE, null, resumeData) // THEN we register a timeout - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) - assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) } @Test fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() { // WHEN we have a resume control - mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) + loadMediaData(PACKAGE, null, resumeData) // AND that media is resumed val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() whenever(mediaController.playbackState).thenReturn(playingState) - mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) + loadMediaData(oldKey = PACKAGE, data = mediaData) // THEN the timeout length is changed to a regular media control - assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @Test @@ -401,7 +410,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaTimeoutListener.onMediaDataRemoved(PACKAGE, false) // THEN the timeout runnable is cancelled - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) } @Test @@ -427,6 +436,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // When the playback state changes, and has different actions val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked verify(stateCallback).invoke(eq(KEY), eq(playingState!!)) @@ -463,6 +473,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { .addCustomAction(customTwo) .build() mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions) + assertThat(uiExecutor.runAllReady()).isEqualTo(1) // Then the callback is invoked verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!)) @@ -534,6 +545,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { val playingState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() mediaCallbackCaptor.value.onPlaybackStateChanged(playingState) + uiExecutor.runAllReady() // Then the callback is invoked verify(stateCallback).invoke(eq(KEY), eq(playingState!!)) @@ -567,7 +579,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // And we doze past the scheduled timeout val time = clock.currentTimeMillis() clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // Then when no longer dozing, the timeout runs immediately dozingCallbackCaptor.value.onDozingChanged(false) @@ -576,7 +588,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // and cancel any later scheduled timeout verify(logger).logTimeoutCancelled(eq(KEY), any()) - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) } @Test @@ -592,12 +604,12 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // And we doze, but not past the scheduled timeout clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // Then when no longer dozing, the timeout remains scheduled dozingCallbackCaptor.value.onDozingChanged(false) verify(timeoutCallback, never()).invoke(eq(KEY), eq(true)) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) } @Test @@ -610,8 +622,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime) mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(executor.numPending()).isEqualTo(1) - assertThat(executor.advanceClockToNext()).isEqualTo(duration) + assertThat(mainExecutor.numPending()).isEqualTo(1) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration) } @Test @@ -619,7 +631,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { // Given a pending timeout testSmartspaceDataLoaded_schedulesTimeout() - executor.runAllReady() + mainExecutor.runAllReady() verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true)) } @@ -634,14 +646,14 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime) mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) val expiryLonger = expireTime + duration whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger) mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(executor.numPending()).isEqualTo(1) - assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2) + assertThat(mainExecutor.numPending()).isEqualTo(1) + assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration * 2) } @Test @@ -649,10 +661,10 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) } @Test @@ -667,12 +679,12 @@ class MediaTimeoutListenerTest : SysuiTestCase() { whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime) mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // And we doze past the scheduled timeout val time = clock.currentTimeMillis() clock.setElapsedRealtime(time + duration * 2) - assertThat(executor.numPending()).isEqualTo(1) + assertThat(mainExecutor.numPending()).isEqualTo(1) // Then when no longer dozing, the timeout runs immediately dozingCallbackCaptor.value.onDozingChanged(false) @@ -680,12 +692,18 @@ class MediaTimeoutListenerTest : SysuiTestCase() { verify(logger).logTimeout(eq(SMARTSPACE_KEY)) // and cancel any later scheduled timeout - assertThat(executor.numPending()).isEqualTo(0) + assertThat(mainExecutor.numPending()).isEqualTo(0) } private fun loadMediaDataWithPlaybackState(state: PlaybackState) { whenever(mediaController.playbackState).thenReturn(state) - mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + loadMediaData(data = mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) } + + private fun loadMediaData(key: String = KEY, oldKey: String? = null, data: MediaData) { + mediaTimeoutListener.onMediaDataLoaded(key, oldKey, data) + bgExecutor.runAllReady() + uiExecutor.runAllReady() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt index 1260a65b9c1c..68a5d9361046 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt @@ -79,6 +79,7 @@ import com.android.systemui.media.controls.shared.model.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.MediaNotificationAction import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.ui.binder.SeekBarObserver import com.android.systemui.media.controls.ui.view.GutsViewHolder @@ -236,6 +237,19 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var recProgressBar3: SeekBar @Mock private lateinit var globalSettings: GlobalSettings + private val intent = + Intent().apply { + putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) }) + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + private val pendingIntent = + PendingIntent.getActivity( + mContext, + 0, + intent.setPackage(mContext.packageName), + PendingIntent.FLAG_MUTABLE + ) + @JvmField @Rule val mockito = MockitoJUnit.rule() @Before @@ -989,14 +1003,13 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindNotificationActions() { val icon = context.getDrawable(android.R.drawable.ic_media_play) - val bg = context.getDrawable(R.drawable.qs_media_round_button_background) val actions = listOf( - MediaAction(icon, Runnable {}, "previous", bg), - MediaAction(icon, Runnable {}, "play", bg), - MediaAction(icon, null, "next", bg), - MediaAction(icon, null, "custom 0", bg), - MediaAction(icon, Runnable {}, "custom 1", bg) + MediaNotificationAction(true, actionIntent = pendingIntent, icon, "previous"), + MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"), + MediaNotificationAction(true, actionIntent = null, icon, "next"), + MediaNotificationAction(true, actionIntent = null, icon, "custom 0"), + MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1") ) val state = mediaData.copy( @@ -1684,11 +1697,11 @@ public class MediaControlPanelTest : SysuiTestCase() { fun actionCustom2Click_isLogged() { val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") ) val data = mediaData.copy(actions = actions) @@ -1703,11 +1716,11 @@ public class MediaControlPanelTest : SysuiTestCase() { fun actionCustom3Click_isLogged() { val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") ) val data = mediaData.copy(actions = actions) @@ -1722,11 +1735,11 @@ public class MediaControlPanelTest : SysuiTestCase() { fun actionCustom4Click_isLogged() { val actions = listOf( - MediaAction(null, Runnable {}, "action 0", null), - MediaAction(null, Runnable {}, "action 1", null), - MediaAction(null, Runnable {}, "action 2", null), - MediaAction(null, Runnable {}, "action 3", null), - MediaAction(null, Runnable {}, "action 4", null) + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"), + MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4") ) val data = mediaData.copy(actions = actions) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java index c1cf91d6520c..bc0ec2d784f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java @@ -22,6 +22,7 @@ import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PEN import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX; +import static com.android.systemui.Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES; import static com.google.common.truth.Truth.assertThat; @@ -75,6 +76,8 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; +import com.google.common.truth.Truth; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -95,7 +98,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + FLAG_QS_QUICK_REBIND_ACTIVE_TILES); } private final PackageManagerAdapter mMockPackageManagerAdapter = @@ -154,7 +158,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); } @After @@ -169,12 +174,12 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mStateManager.handleDestroy(); } - private void setPackageEnabled(boolean enabled) throws Exception { + private void setPackageEnabledAndActive(boolean enabled, boolean active) throws Exception { ServiceInfo defaultServiceInfo = null; if (enabled) { defaultServiceInfo = new ServiceInfo(); defaultServiceInfo.metaData = new Bundle(); - defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, true); + defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active); defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, true); } when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), anyInt())) @@ -186,6 +191,10 @@ public class TileLifecycleManagerTest extends SysuiTestCase { .thenReturn(defaultPackageInfo); } + private void setPackageEnabled(boolean enabled) throws Exception { + setPackageEnabledAndActive(enabled, true); + } + private void setPackageInstalledForUser( boolean installed, boolean active, @@ -396,18 +405,125 @@ public class TileLifecycleManagerTest extends SysuiTestCase { } @Test - public void testKillProcess() throws Exception { + public void testKillProcessWhenTileServiceIsNotActive() throws Exception { + setPackageEnabledAndActive(true, false); mStateManager.onStartListening(); mStateManager.executeSetBindService(true); mExecutor.runAllReady(); + verifyBind(1); + verify(mMockTileService, times(1)).onStartListening(); + mStateManager.onBindingDied(mTileServiceComponentName); mExecutor.runAllReady(); - mClock.advanceTime(5000); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + + // still 4 seconds left because non active tile service rebind time is 5 seconds + Truth.assertThat(mContext.isBound(mTileServiceComponentName)).isFalse(); + + mClock.advanceTime(4000); // 5 seconds delay for nonActive service rebinding + mExecutor.runAllReady(); + verifyBind(2); + verify(mMockTileService, times(2)).onStartListening(); + } + + @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES) + @Test + public void testKillProcessWhenTileServiceIsActive_withRebindFlagOn() throws Exception { + mStateManager.onStartListening(); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + verifyBind(1); + verify(mMockTileService, times(1)).onStartListening(); + + mStateManager.onBindingDied(mTileServiceComponentName); + mExecutor.runAllReady(); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + + // Two calls: one for the first bind, one for the restart. + verifyBind(2); + verify(mMockTileService, times(2)).onStartListening(); + } + + @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES) + @Test + public void testKillProcessWhenTileServiceIsActive_withRebindFlagOff() throws Exception { + mStateManager.onStartListening(); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + verifyBind(1); + verify(mMockTileService, times(1)).onStartListening(); + + mStateManager.onBindingDied(mTileServiceComponentName); + mExecutor.runAllReady(); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + verifyBind(0); // the rebind happens after 4 more seconds + + mClock.advanceTime(4000); + mExecutor.runAllReady(); + verifyBind(1); + } + + @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES) + @Test + public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOn_delaysSecondRebind() + throws Exception { + mStateManager.onStartListening(); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + verifyBind(1); + verify(mMockTileService, times(1)).onStartListening(); + + mStateManager.onBindingDied(mTileServiceComponentName); + mExecutor.runAllReady(); + mClock.advanceTime(1000); mExecutor.runAllReady(); // Two calls: one for the first bind, one for the restart. verifyBind(2); verify(mMockTileService, times(2)).onStartListening(); + + mStateManager.onBindingDied(mTileServiceComponentName); + mExecutor.runAllReady(); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + // because active tile will take 5 seconds to bind the second time, not 1 + verifyBind(0); + + mClock.advanceTime(4000); + mExecutor.runAllReady(); + verifyBind(1); + } + + @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES) + @Test + public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOff_rebindsFromFirstKill() + throws Exception { + mStateManager.onStartListening(); + mStateManager.executeSetBindService(true); + mExecutor.runAllReady(); + verifyBind(1); + verify(mMockTileService, times(1)).onStartListening(); + + mStateManager.onBindingDied(mTileServiceComponentName); // rebind scheduled for 5 seconds + mExecutor.runAllReady(); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + + verifyBind(0); // it would bind in 4 more seconds + + mStateManager.onBindingDied(mTileServiceComponentName); // this does not affect the rebind + mExecutor.runAllReady(); + mClock.advanceTime(1000); + mExecutor.runAllReady(); + + verifyBind(0); // only 2 seconds passed from first kill + + mClock.advanceTime(3000); + mExecutor.runAllReady(); + verifyBind(1); // the rebind scheduled 5 seconds from the first kill should now happen } @Test @@ -510,7 +626,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); manager.executeSetBindService(true); mExecutor.runAllReady(); @@ -533,7 +650,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); manager.executeSetBindService(true); mExecutor.runAllReady(); @@ -556,7 +674,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); manager.executeSetBindService(true); mExecutor.runAllReady(); @@ -581,7 +700,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); manager.executeSetBindService(true); mExecutor.runAllReady(); @@ -607,7 +727,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); assertThat(manager.isActiveTile()).isTrue(); } @@ -626,7 +747,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); assertThat(manager.isActiveTile()).isTrue(); } @@ -644,7 +766,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); assertThat(manager.isToggleableTile()).isTrue(); } @@ -663,7 +786,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); assertThat(manager.isToggleableTile()).isTrue(); } @@ -682,7 +806,8 @@ public class TileLifecycleManagerTest extends SysuiTestCase { mUser, mActivityManager, mDeviceIdleController, - mExecutor); + mExecutor, + mClock); assertThat(manager.isToggleableTile()).isFalse(); assertThat(manager.isActiveTile()).isFalse(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java index 76c8cf081262..7d41a20e628f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java @@ -16,6 +16,8 @@ package com.android.systemui.qs.tiles; +import static com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.LAUNCH_SOURCE_QS_TILE; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -151,7 +153,7 @@ public class HearingDevicesTileTest extends SysuiTestCase { mTile.handleClick(expandable); mTestableLooper.processAllMessages(); - verify(mHearingDevicesDialogManager).showDialog(expandable); + verify(mHearingDevicesDialogManager).showDialog(expandable, LAUNCH_SOURCE_QS_TILE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index 73548baad377..ca518f9bc5d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.recordissue.TraceurMessageSender import com.android.systemui.res.R +import com.android.systemui.screenrecord.RecordingController import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.phone.SystemUIDialog @@ -65,6 +66,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var qsEventLogger: QsEventLogger @Mock private lateinit var metricsLogger: MetricsLogger @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var recordingController: RecordingController @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var qsLogger: QSLogger @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil @@ -109,6 +111,7 @@ class RecordIssueTileTest : SysuiTestCase() { Executors.newSingleThreadExecutor(), issueRecordingState, delegateFactory, + recordingController, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt new file mode 100644 index 000000000000..57cfe1b9e902 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 The Android Open 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.recordissue + +import android.app.IActivityManager +import android.app.NotificationManager +import android.net.Uri +import android.os.UserHandle +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor +import com.android.systemui.settings.UserContextProvider +import com.android.systemui.settings.userFileManager +import com.android.systemui.settings.userTracker +import com.android.traceur.TraceConfig +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() { + + private val kosmos = Kosmos().also { it.testCase = this } + private val bgExecutor = kosmos.fakeExecutor + private val userContextProvider: UserContextProvider = kosmos.userTracker + private val dialogTransitionAnimator: DialogTransitionAnimator = kosmos.dialogTransitionAnimator + private lateinit var traceurMessageSender: TraceurMessageSender + private val issueRecordingState = + IssueRecordingState(kosmos.userTracker, kosmos.userFileManager) + + private val iActivityManager = mock<IActivityManager>() + private val notificationManager = mock<NotificationManager>() + private val panelInteractor = mock<PanelInteractor>() + + private lateinit var underTest: IssueRecordingServiceCommandHandler + + @Before + fun setup() { + traceurMessageSender = mock<TraceurMessageSender>() + underTest = + IssueRecordingServiceCommandHandler( + bgExecutor, + dialogTransitionAnimator, + panelInteractor, + traceurMessageSender, + issueRecordingState, + iActivityManager, + notificationManager, + userContextProvider + ) + } + + @Test + fun startsTracing_afterReceivingActionStartCommand() { + underTest.handleStartCommand() + bgExecutor.runAllReady() + + Truth.assertThat(issueRecordingState.isRecording).isTrue() + verify(traceurMessageSender).startTracing(any<TraceConfig>()) + } + + @Test + fun stopsTracing_afterReceivingStopTracingCommand() { + underTest.handleStopCommand(mContext.contentResolver) + bgExecutor.runAllReady() + + Truth.assertThat(issueRecordingState.isRecording).isFalse() + verify(traceurMessageSender).stopTracing() + } + + @Test + fun cancelsNotification_afterReceivingShareCommand() { + underTest.handleShareCommand(0, null, mContext) + bgExecutor.runAllReady() + + verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>()) + } + + @Test + fun requestBugreport_afterReceivingShareCommand_withTakeBugreportTrue() { + issueRecordingState.takeBugreport = true + val uri = mock<Uri>() + + underTest.handleShareCommand(0, uri, mContext) + bgExecutor.runAllReady() + + verify(iActivityManager).requestBugReportWithExtraAttachment(uri) + } + + @Test + fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() { + issueRecordingState.takeBugreport = false + val uri = mock<Uri>() + + underTest.handleShareCommand(0, uri, mContext) + bgExecutor.runAllReady() + + verify(traceurMessageSender).shareTraces(mContext, uri) + } + + @Test + fun closesShade_afterReceivingShareCommand() { + val uri = mock<Uri>() + + underTest.handleShareCommand(0, uri, mContext) + bgExecutor.runAllReady() + + verify(panelInteractor).collapsePanels() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt deleted file mode 100644 index 5e07aef7d8e9..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * 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 - -import android.app.Notification -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.net.Uri -import android.os.UserHandle -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType -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 java.util.concurrent.CompletableFuture -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test - -@SmallTest -class SaveImageInBackgroundTaskTest : SysuiTestCase() { - private val imageExporter = mock<ImageExporter>() - private val smartActions = mock<ScreenshotSmartActions>() - private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>() - private val saveImageData = SaveImageInBackgroundTask.SaveImageInBackgroundData() - private val testScreenshotId: String = "testScreenshotId" - private val testBitmap = mock<Bitmap>() - private val testUser = UserHandle.getUserHandleForUid(0) - private val testIcon = mock<Icon>() - private val testImageTime = 1234.toLong() - private val flags = FakeFeatureFlags() - - private val smartActionsUriFuture = mock<CompletableFuture<List<Notification.Action>>>() - private val smartActionsFuture = mock<CompletableFuture<List<Notification.Action>>>() - - private val testUri: Uri = Uri.parse("testUri") - private val intent = - Intent(Intent.ACTION_SEND) - .setComponent( - ComponentName.unflattenFromString( - "com.google.android.test/com.google.android.test.TestActivity" - ) - ) - private val immutablePendingIntent = - PendingIntent.getBroadcast( - mContext, - 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - private val mutablePendingIntent = - PendingIntent.getBroadcast( - mContext, - 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - - private val saveImageTask = - SaveImageInBackgroundTask( - mContext, - flags, - imageExporter, - smartActions, - saveImageData, - smartActionsProvider, - ) - - @Before - fun setup() { - whenever( - smartActions.getSmartActionsFuture( - eq(testScreenshotId), - any(Uri::class.java), - eq(testBitmap), - eq(smartActionsProvider), - any(ScreenshotSmartActionType::class.java), - any(Boolean::class.java), - eq(testUser) - ) - ) - .thenReturn(smartActionsUriFuture) - whenever( - smartActions.getSmartActionsFuture( - eq(testScreenshotId), - eq(null), - eq(testBitmap), - eq(smartActionsProvider), - any(ScreenshotSmartActionType::class.java), - any(Boolean::class.java), - eq(testUser) - ) - ) - .thenReturn(smartActionsFuture) - } - - @Test - fun testQueryQuickShare_noAction() { - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(ArrayList<Notification.Action>()) - - val quickShareAction = - saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri) - - assertNull(quickShareAction) - } - - @Test - fun testQueryQuickShare_withActions() { - val actions = ArrayList<Notification.Action>() - actions.add(constructAction("Action One", mutablePendingIntent)) - actions.add(constructAction("Action Two", mutablePendingIntent)) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)!! - - assertEquals("Action One", quickShareAction.title) - assertEquals(mutablePendingIntent, quickShareAction.actionIntent) - } - - @Test - fun testCreateQuickShareAction_originalWasNull_returnsNull() { - val quickShareAction = - saveImageTask.createQuickShareAction( - null, - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser - ) - - assertNull(quickShareAction) - } - - @Test - fun testCreateQuickShareAction_immutableIntentDifferentAction_returnsNull() { - val actions = ArrayList<Notification.Action>() - actions.add(constructAction("New Test Action", immutablePendingIntent)) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - val origAction = constructAction("Old Test Action", immutablePendingIntent) - - val quickShareAction = - saveImageTask.createQuickShareAction( - origAction, - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser, - ) - - assertNull(quickShareAction) - } - - @Test - fun testCreateQuickShareAction_mutableIntent_returnsSafeIntent() { - val actions = ArrayList<Notification.Action>() - val action = constructAction("Action One", mutablePendingIntent) - actions.add(action) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.createQuickShareAction( - constructAction("Test Action", mutablePendingIntent), - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser - ) - val quickSharePendingIntent = - quickShareAction.actionIntent.intent.extras!!.getParcelable( - SmartActionsReceiver.EXTRA_ACTION_INTENT, - PendingIntent::class.java - ) - - assertEquals("Test Action", quickShareAction.title) - assertEquals(mutablePendingIntent, quickSharePendingIntent) - } - - @Test - fun testCreateQuickShareAction_immutableIntent_returnsSafeIntent() { - val actions = ArrayList<Notification.Action>() - val action = constructAction("Test Action", immutablePendingIntent) - actions.add(action) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.createQuickShareAction( - constructAction("Test Action", immutablePendingIntent), - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser, - )!! - - assertEquals("Test Action", quickShareAction.title) - assertEquals( - immutablePendingIntent, - quickShareAction.actionIntent.intent.extras!!.getParcelable( - SmartActionsReceiver.EXTRA_ACTION_INTENT, - PendingIntent::class.java - ) - ) - } - - private fun constructAction(title: String, intent: PendingIntent): Notification.Action { - return Notification.Action.Builder(testIcon, title, intent).build() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt index 263b0017221a..78764c27327c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt @@ -79,21 +79,6 @@ class UserTrackerImplReceiveTest : SysuiTestCase() { @Test fun callsCallbackAndUpdatesProfilesWhenAnIntentReceived() = runTest { - tracker = - UserTrackerImpl( - context, - { fakeFeatures }, - userManager, - iActivityManager, - dumpManager, - this, - testDispatcher, - handler - ) - tracker.initialize(0) - tracker.addCallback(callback, executor) - val profileID = tracker.userId + 10 - `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation -> val id = invocation.getArgument<Int>(0) val info = UserInfo(id, "", UserInfo.FLAG_FULL) @@ -109,6 +94,21 @@ class UserTrackerImplReceiveTest : SysuiTestCase() { listOf(info, infoProfile) } + tracker = + UserTrackerImpl( + context, + { fakeFeatures }, + userManager, + iActivityManager, + dumpManager, + this, + testDispatcher, + handler + ) + tracker.initialize(0) + tracker.addCallback(callback, executor) + val profileID = tracker.userId + 10 + tracker.onReceive(context, Intent(intentAction)) verify(callback, times(0)).onUserChanged(anyInt(), any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 9481e5a52098..e0c4ab737511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -431,7 +431,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFakeKeyguardRepository, mKeyguardTransitionInteractor, mPowerInteractor, - mShadeRepository, new FakeUserSetupRepository(), mock(UserSwitcherInteractor.class), new ShadeInteractorLegacyImpl( @@ -447,8 +446,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { () -> mLargeScreenHeaderHelper ), mShadeRepository - ) - ); + ), + mKosmos.getShadeModeInteractor()); SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 3f6617b32131..a52f1737117a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -217,7 +217,6 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mKeyguardRepository, keyguardTransitionInteractor, powerInteractor, - mShadeRepository, new FakeUserSetupRepository(), mUserSwitcherInteractor, new ShadeInteractorLegacyImpl( @@ -232,8 +231,8 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { deviceEntryUdfpsInteractor, () -> mLargeScreenHeaderHelper), mShadeRepository - ) - ); + ), + mKosmos.getShadeModeInteractor()); mActiveNotificationsInteractor = new ActiveNotificationsInteractor( new ActiveNotificationListRepository(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 01a3d36a05ec..1d74331e429b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1112,9 +1112,11 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); + // Returning false means unable to show the bouncer when(mPrimaryBouncerInteractor.show(true)).thenReturn(false); when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) .thenReturn(KeyguardState.LOCKSCREEN); + mStatusBarKeyguardViewManager.onStartedWakingUp(); reset(mCentralSurfaces); // Advance past reattempts @@ -1127,6 +1129,23 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer + @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART) + public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() { + when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) + .thenReturn(KeyguardState.LOCKSCREEN); + mStatusBarKeyguardViewManager.onStartedGoingToSleep(); + + reset(mCentralSurfaces); + reset(mPrimaryBouncerInteractor); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard( + /* hideBouncerWhenShowing= */true, false); + verify(mCentralSurfaces).showKeyguard(); + verify(mPrimaryBouncerInteractor).hide(); + } + + + @Test + @DisableSceneContainer public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() { boolean isFalsingReset = false; when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( 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 fe408e3246c8..763449028f28 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 @@ -817,7 +817,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { captor.lastValue.onReceive(context, intent) // spnIntent() sets all values to true and test strings - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) job.cancel() } @@ -852,7 +852,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { verify(context).registerReceiver(captor.capture(), any()) captor.lastValue.onReceive(context, intent) - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) // WHEN an intent with a different subId is sent val wrongSubIntent = spnIntent(subId = 101) @@ -860,7 +860,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { captor.lastValue.onReceive(context, wrongSubIntent) // THEN the previous intent's name is still used - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) job.cancel() } @@ -902,7 +902,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { verify(context).registerReceiver(captor.capture(), any()) captor.lastValue.onReceive(context, intent) - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) val intentWithoutInfo = spnIntent( @@ -961,7 +961,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { // The value is still there despite no active subscribers assertThat(underTest.networkName.value) - .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) + .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) } @Test @@ -986,7 +986,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) - fun networkName_allFieldsSet_doesNotUseDataSpn() = + fun networkName_allFieldsSet_prioritizesDataSpnOverSpn() = testScope.runTest { val latest by collectLastValue(underTest.networkName) val captor = argumentCaptor<BroadcastReceiver>() @@ -1002,6 +1002,27 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { plmn = PLMN, ) captor.lastValue.onReceive(context, intent) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + } + + @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) + fun networkName_spnAndPlmn_fallbackToSpnWhenNullDataSpn() = + testScope.runTest { + val latest by collectLastValue(underTest.networkName) + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + + val intent = + spnIntent( + subId = SUB_1_ID, + showSpn = true, + spn = SPN, + dataSpn = null, + showPlmn = true, + plmn = PLMN, + ) + captor.lastValue.onReceive(context, intent) assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) } @@ -1043,7 +1064,27 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { plmn = PLMN, ) captor.lastValue.onReceive(context, intent) - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + } + + @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) + fun networkName_showPlmn_plmnNotNull_showSpn_spnNotNull_dataSpnNull() = + testScope.runTest { + val latest by collectLastValue(underTest.networkName) + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + val intent = + spnIntent( + subId = SUB_1_ID, + showSpn = true, + spn = SPN, + dataSpn = null, + showPlmn = true, + plmn = PLMN, + ) + captor.lastValue.onReceive(context, intent) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN")) } @Test @@ -1102,10 +1143,50 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { plmn = null, ) captor.lastValue.onReceive(context, intent) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$DATA_SPN")) + } + + @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) + fun networkName_showPlmn_plmnNull_showSpn_dataSpnNull() = + testScope.runTest { + val latest by collectLastValue(underTest.networkName) + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + val intent = + spnIntent( + subId = SUB_1_ID, + showSpn = true, + spn = SPN, + dataSpn = null, + showPlmn = true, + plmn = null, + ) + captor.lastValue.onReceive(context, intent) assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$SPN")) } @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) + fun networkName_showPlmn_plmnNull_showSpn_bothSpnNull() = + testScope.runTest { + val latest by collectLastValue(underTest.networkName) + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + val intent = + spnIntent( + subId = SUB_1_ID, + showSpn = true, + spn = null, + dataSpn = null, + showPlmn = true, + plmn = null, + ) + captor.lastValue.onReceive(context, intent) + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + } + + @Test @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) fun networkName_showPlmn_plmnNull_showSpn_flagOff() = testScope.runTest { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java index 035847497178..3041240e8c86 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java @@ -19,6 +19,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.UserHandle; @@ -40,6 +41,7 @@ public class SysuiTestableContext extends TestableContext { @GuardedBy("mRegisteredReceivers") private final Set<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>(); private final Map<UserHandle, Context> mContextForUser = new HashMap<>(); + private final Map<String, Context> mContextForPackage = new HashMap<>(); public SysuiTestableContext(Context base) { super(base); @@ -175,4 +177,22 @@ public class SysuiTestableContext extends TestableContext { } return super.createContextAsUser(user, flags); } + + /** + * Sets a Context object that will be returned as the result of {@link #createPackageContext} + * for a specific {@code packageName}. + */ + public void prepareCreatePackageContext(String packageName, Context context) { + mContextForPackage.put(packageName, context); + } + + @Override + public Context createPackageContext(String packageName, int flags) + throws PackageManager.NameNotFoundException { + Context packageContext = mContextForPackage.get(packageName); + if (packageContext != null) { + return packageContext; + } + return super.createPackageContext(packageName, flags); + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt index 2a0e764279d6..a6394631d236 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt @@ -16,25 +16,31 @@ package com.android.systemui.accessibility.data.repository +import com.android.systemui.accessibility.data.model.CaptioningModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FakeCaptioningRepository : CaptioningRepository { - private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false) - override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> - get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow() - - private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false) - override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> - get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow() + private val mutableCaptioningModel = MutableStateFlow<CaptioningModel?>(null) + override val captioningModel: StateFlow<CaptioningModel?> = mutableCaptioningModel.asStateFlow() override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) { - mutableIsSystemAudioCaptioningEnabled.value = isEnabled + mutableCaptioningModel.value = + CaptioningModel( + isSystemAudioCaptioningEnabled = isEnabled, + isSystemAudioCaptioningUiEnabled = + mutableCaptioningModel.value?.isSystemAudioCaptioningUiEnabled == true, + ) } - fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) { - mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled + fun setIsSystemAudioCaptioningUiEnabled(isEnabled: Boolean) { + mutableCaptioningModel.value = + CaptioningModel( + isSystemAudioCaptioningEnabled = + mutableCaptioningModel.value?.isSystemAudioCaptioningEnabled == true, + isSystemAudioCaptioningUiEnabled = isEnabled, + ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt index 5ced578ad974..3087d01a2479 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt @@ -26,6 +26,7 @@ import com.android.systemui.bouncer.data.repository.emergencyServicesRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.telephony.domain.interactor.telephonyInteractor import com.android.systemui.user.domain.interactor.selectedUserInteractor @@ -50,5 +51,6 @@ val Kosmos.bouncerActionButtonInteractor by Fixture { }, metricsLogger = metricsLogger, dozeLogger = mock(), + sceneInteractor = { sceneInteractor }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt index f5a05b44d2cf..4f5c32abd2f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/msdl/MSDLPlayerKosmos.kt @@ -17,5 +17,7 @@ package com.android.systemui.haptics.msdl import com.android.systemui.kosmos.Kosmos +import com.google.android.msdl.domain.MSDLPlayer -val Kosmos.msdlPlayer by Kosmos.Fixture { FakeMSDLPlayer() } +var Kosmos.msdlPlayer: MSDLPlayer by Kosmos.Fixture { fakeMSDLPlayer } +val Kosmos.fakeMSDLPlayer by Kosmos.Fixture { FakeMSDLPlayer() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt index 1e95fc12bdb5..740d8919cbc0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerViewModel import com.android.systemui.keyguard.ui.viewmodel.alternateBouncerWindowViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.statusbar.gesture.TapGestureDetector import com.android.systemui.util.mockito.mock @@ -64,6 +65,7 @@ private val Kosmos.alternateBouncerDependencies by }, messageAreaViewModel = mock<AlternateBouncerMessageAreaViewModel>(), powerInteractor = powerInteractor, + touchLogBuffer = logcatLogBuffer(), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 851a378f3165..c60305e85b22 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -36,7 +36,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor -import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.haptics.msdl.fakeMSDLPlayer import com.android.systemui.haptics.qs.qsLongPressEffect import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -61,6 +61,7 @@ import com.android.systemui.scene.shared.model.sceneDataSource import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.shadeController import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel @@ -155,5 +156,6 @@ class KosmosJavaAdapter() { val scrimController by lazy { kosmos.scrimController } val scrimStartable by lazy { kosmos.scrimStartable } val sceneContainerOcclusionInteractor by lazy { kosmos.sceneContainerOcclusionInteractor } - val msdlPlayer by lazy { kosmos.msdlPlayer } + val msdlPlayer by lazy { kosmos.fakeMSDLPlayer } + val shadeModeInteractor by lazy { kosmos.shadeModeInteractor } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt index a5690a0fa560..cb7750f55647 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt @@ -24,7 +24,6 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.mediaFlags -import com.android.systemui.plugins.activityStarter val Kosmos.mediaDataLoader by Kosmos.Fixture { @@ -32,7 +31,6 @@ val Kosmos.mediaDataLoader by testableContext, testDispatcher, testScope, - activityStarter, fakeMediaControllerFactory, mediaFlags, imageLoader, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt index 632436a4574a..174e6532abcf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt @@ -26,6 +26,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.media.controls.data.repository.mediaDataRepository +import com.android.systemui.media.controls.shared.mediaLogger import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider import com.android.systemui.media.controls.util.fakeMediaControllerFactory import com.android.systemui.media.controls.util.mediaFlags @@ -60,5 +61,6 @@ val Kosmos.mediaDataProcessor by keyguardUpdateMonitor = keyguardUpdateMonitor, mediaDataRepository = mediaDataRepository, mediaDataLoader = { mediaDataLoader }, + mediaLogger = mediaLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt index b7660e05ee91..b33edf97bd55 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt @@ -28,6 +28,8 @@ val Kosmos.mediaTimeoutListener by Kosmos.Fixture { MediaTimeoutListener( mediaControllerFactory = fakeMediaControllerFactory, + bgExecutor = fakeExecutor, + uiExecutor = fakeExecutor, mainExecutor = fakeExecutor, logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")), statusBarStateController = statusBarStateController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt index a0fc76b3d7de..4978558ff8a2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.fakeSystemClock val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by Kosmos.Fixture { @@ -39,6 +40,7 @@ val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by activityManager, mock(), fakeExecutor, + fakeSystemClock, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 55f3ed7062aa..874463819c73 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -12,6 +12,8 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.FakeOverlay import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector +import com.android.systemui.shade.domain.interactor.shadeInteractor import kotlinx.coroutines.flow.MutableStateFlow var Kosmos.sceneKeys by Fixture { @@ -70,6 +72,8 @@ val Kosmos.sceneContainerViewModel by Fixture { sceneInteractor = sceneInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, + shadeInteractor = shadeInteractor, + splitEdgeDetector = splitEdgeDetector, motionEventHandlerReceiver = {}, logger = sceneLogger ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt index ae33aead67a7..d17b5750b937 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt @@ -24,7 +24,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.sceneFamilyResolvers: Map<SceneKey, SceneResolver> @@ -48,7 +48,7 @@ val Kosmos.notifShadeSceneFamilyResolver by Kosmos.Fixture { NotifShadeSceneFamilyResolver( applicationScope = applicationCoroutineScope, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, ) } @@ -56,6 +56,6 @@ val Kosmos.quickSettingsSceneFamilyResolver by Kosmos.Fixture { QuickSettingsSceneFamilyResolver( applicationScope = applicationCoroutineScope, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index b612a8b5893a..9a5698cfb8ca 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -27,6 +27,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInt import com.android.systemui.deviceentry.domain.interactor.deviceEntryHapticsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor @@ -86,5 +87,6 @@ val Kosmos.sceneContainerStartable by Fixture { statusBarStateController = sysuiStatusBarStateController, alternateBouncerInteractor = alternateBouncerInteractor, vibratorHelper = vibratorHelper, + msdlPlayer = msdlPlayer, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt new file mode 100644 index 000000000000..e0b529261c4d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open 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.scene.ui.viewmodel + +import androidx.compose.ui.unit.dp +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.domain.interactor.shadeInteractor + +var Kosmos.splitEdgeDetector: SplitEdgeDetector by + Kosmos.Fixture { + SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + edgeSize = 40.dp, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index 54208b9cdaef..04d930c72792 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -53,7 +53,7 @@ val Kosmos.shadeInteractorLegacyImpl by scope = applicationCoroutineScope, keyguardRepository = keyguardRepository, sharedNotificationContainerInteractor = sharedNotificationContainerInteractor, - repository = shadeRepository + repository = shadeRepository, ) } var Kosmos.shadeInteractor: ShadeInteractor by Kosmos.Fixture { shadeInteractorImpl } @@ -70,6 +70,6 @@ val Kosmos.shadeInteractorImpl by userSetupRepository = userSetupRepository, userSwitcherInteractor = userSwitcherInteractor, baseShadeInteractor = baseShadeInteractor, - shadeRepository = shadeRepository, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt new file mode 100644 index 000000000000..7892e962d63d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open 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.shade.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.shade.data.repository.shadeRepository + +val Kosmos.shadeModeInteractor by Fixture { + ShadeModeInteractorImpl( + applicationScope = applicationCoroutineScope, + repository = shadeRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt new file mode 100644 index 000000000000..78763f97adc3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open 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.user.utils + +import android.os.UserHandle + +class FakeUserScopedService<T>(private val defaultImplementation: T) : UserScopedService<T> { + + private val implementations = mutableMapOf<UserHandle, T>() + + fun addImplementation(user: UserHandle, implementation: T) { + implementations[user] = implementation + } + + fun removeImplementation(user: UserHandle): T? = implementations.remove(user) + + override fun forUser(user: UserHandle): T = + implementations.getOrDefault(user, defaultImplementation) +} diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 26b0f617d971..136738fcb343 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.graphics.GraphicBuffer; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.hardware.SyncFence; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraExtensionCharacteristics; @@ -2525,6 +2526,19 @@ public class CameraExtensionsProxyService extends Service { } @Override + public SyncFence getFence() { + if (mParcelImage.fence != null) { + try { + return SyncFence.create(mParcelImage.fence.dup()); + } catch (IOException e) { + Log.e(TAG, "Failed to parcel buffer fence!"); + } + } + + return SyncFence.createEmpty(); + } + + @Override protected final void finalize() throws Throwable { try { close(); diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 9b0c8e554d64..eebe5e9fc054 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -94,6 +94,9 @@ java_library { libs: [ "ravenwood-runtime-common-ravenwood", ], + static_libs: [ + "framework-annotations-lib", // should it be "libs" instead? + ], visibility: ["//visibility:private"], } @@ -127,6 +130,7 @@ java_library { libs: [ "framework-minus-apex.ravenwood", "ravenwood-junit", + "ravenwood-helper-libcore-runtime", ], visibility: ["//visibility:private"], } diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java new file mode 100644 index 000000000000..b582ccf7b656 --- /dev/null +++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwood.annotation; + +import static java.lang.annotation.ElementType.METHOD; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY + * QUESTIONS ABOUT IT. + * + * TODO: Javadoc + * + * @hide + */ +@Target({METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface RavenwoodRedirect { +} diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java index 4b9cf85e16fa..bee9222ae5eb 100644 --- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java +++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java @@ -31,6 +31,6 @@ import java.lang.annotation.Target; */ @Target({TYPE}) @Retention(RetentionPolicy.CLASS) -public @interface RavenwoodNativeSubstitutionClass { +public @interface RavenwoodRedirectionClass { String value(); } diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java new file mode 100644 index 000000000000..f9794ad5941e --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that throws from @AfterClass. + * + * Tradefed would ignore it, so instead RavenwoodAwareTestRunner would detect it and kill + * the self (test) process. + * + * Unfortunately, this behavior can't easily be tested from within this class, so for now + * it's only used for a manual test, which you can run by removing the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodAfterClassFailureTest { + public RavenwoodAfterClassFailureTest(String param) { + } + + @AfterClass + public static void afterClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + throw new RuntimeException("FAILURE"); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java new file mode 100644 index 000000000000..61fb06865545 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails in assumption in @BeforeClass. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * "ASSUMPTION_FAILED". + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodBeforeClassAssumptionFailureTest { + public RavenwoodBeforeClassAssumptionFailureTest(String param) { + } + + @BeforeClass + public static void beforeClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + Assume.assumeTrue(false); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java new file mode 100644 index 000000000000..626ce8198eeb --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails throws from @BeforeClass. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * a "FAILURE" runtime exception. + * + * In order to run the test, you'll need to remove the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodBeforeClassFailureTest { + public static final String TAG = "RavenwoodBeforeClassFailureTest"; + + public RavenwoodBeforeClassFailureTest(String param) { + } + + @BeforeClass + public static void beforeClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + throw new RuntimeException("FAILURE"); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java new file mode 100644 index 000000000000..dc949c466110 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import static org.junit.Assume.assumeTrue; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails in assumption from a class rule. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * "ASSUMPTION_FAILED". + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodClassRuleAssumptionFailureTest { + public static final String TAG = "RavenwoodClassRuleFailureTest"; + + @ClassRule + public static final TestRule sClassRule = new TestRule() { + @Override + public Statement apply(Statement base, Description description) { + if (!isOnRavenwood()) { + return base; // Just run the test as-is on a real device. + } + + assumeTrue(false); + return null; // unreachable + } + }; + + public RavenwoodClassRuleAssumptionFailureTest(String param) { + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java new file mode 100644 index 000000000000..9996bec41525 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open 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.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails throws from a class rule. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * a "FAILURE" runtime exception. + * + * In order to run the test, you'll need to remove the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodClassRuleFailureTest { + public static final String TAG = "RavenwoodClassRuleFailureTest"; + + @ClassRule + public static final TestRule sClassRule = new TestRule() { + @Override + public Statement apply(Statement base, Description description) { + if (!isOnRavenwood()) { + return base; // Just run the test as-is on a real device. + } + + throw new RuntimeException("FAILURE"); + } + }; + + public RavenwoodClassRuleFailureTest(String param) { + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java index f237ba908507..6d21e440e911 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -27,20 +27,30 @@ import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.os.RuntimeInit; +import com.android.ravenwood.common.RavenwoodCommonUtils; + import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runners.model.TestClass; +import java.util.Stack; + /** * Provide hook points created by {@link RavenwoodAwareTestRunner}. */ public class RavenwoodAwareTestRunnerHook { - private static final String TAG = "RavenwoodAwareTestRunnerHook"; + private static final String TAG = RavenwoodAwareTestRunner.TAG; private RavenwoodAwareTestRunnerHook() { } private static RavenwoodTestStats sStats; // lazy initialization. + + // Keep track of the current class description. + + // Test classes can be nested because of "Suite", so we need a stack to keep track. + private static final Stack<Description> sClassDescriptions = new Stack<>(); private static Description sCurrentClassDescription; private static RavenwoodTestStats getStats() { @@ -56,20 +66,36 @@ public class RavenwoodAwareTestRunnerHook { * Called when a runner starts, before the inner runner gets a chance to run. */ public static void onRunnerInitializing(Runner runner, TestClass testClass) { + // TODO: Move the initialization code to a better place. + + initOnce(); + // This log call also ensures the framework JNI is loaded. Log.i(TAG, "onRunnerInitializing: testClass=" + testClass.getJavaClass() + " runner=" + runner); - // TODO: Move the initialization code to a better place. + // This is needed to make AndroidJUnit4ClassRunner happy. + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + } + + private static boolean sInitialized = false; + + private static void initOnce() { + if (sInitialized) { + return; + } + sInitialized = true; + + // We haven't initialized liblog yet, so directly write to System.out here. + RavenwoodCommonUtils.log(TAG, "initOnce()"); + + // Redirect stdout/stdin to liblog. + RuntimeInit.redirectLogStreams(); // This will let AndroidJUnit4 use the original runner. System.setProperty("android.junit.runner", "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1"); - - - // This is needed to make AndroidJUnit4ClassRunner happy. - InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); } /** @@ -87,16 +113,17 @@ public class RavenwoodAwareTestRunnerHook { */ public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order) { - Log.i(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); + Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); - if (scope == Scope.Class && order == Order.First) { + if (scope == Scope.Class && order == Order.Outer) { // Keep track of the current class. sCurrentClassDescription = description; + sClassDescriptions.push(description); } // Class-level annotations are checked by the runner already, so we only check // method-level annotations here. - if (scope == Scope.Instance && order == Order.First) { + if (scope == Scope.Instance && order == Order.Outer) { if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood( description, true)) { getStats().onTestFinished(sCurrentClassDescription, description, Result.Skipped); @@ -113,19 +140,22 @@ public class RavenwoodAwareTestRunnerHook { */ public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description, Scope scope, Order order, Throwable th) { - Log.i(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); + Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); - if (scope == Scope.Instance && order == Order.First) { + if (scope == Scope.Instance && order == Order.Outer) { getStats().onTestFinished(sCurrentClassDescription, description, th == null ? Result.Passed : Result.Failed); - } else if (scope == Scope.Class && order == Order.Last) { + } else if (scope == Scope.Class && order == Order.Outer) { getStats().onClassFinished(sCurrentClassDescription); + sClassDescriptions.pop(); + sCurrentClassDescription = + sClassDescriptions.size() == 0 ? null : sClassDescriptions.peek(); } // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error. if (RavenwoodRule.private$ravenwood().isRunningDisabledTests() - && scope == Scope.Instance && order == Order.First) { + && scope == Scope.Instance && order == Order.Outer) { boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood( description, false); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index a2088fd0b77f..c4336762c0b8 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -26,6 +26,7 @@ import android.app.ActivityManager; import android.app.Instrumentation; import android.app.ResourcesManager; import android.content.res.Resources; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.HandlerThread; @@ -36,7 +37,6 @@ import android.view.DisplayAdjustments; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.internal.os.RuntimeInit; import com.android.server.LocalServices; import org.junit.runner.Description; @@ -68,6 +68,8 @@ public class RavenwoodRuleImpl { private static ScheduledFuture<?> sPendingTimeout; + private static long sOriginalIdentityToken = -1; + /** * When enabled, attempt to detect uncaught exceptions from background threads. */ @@ -86,16 +88,28 @@ public class RavenwoodRuleImpl { sPendingUncaughtException.compareAndSet(null, throwable); }; + // TODO: expose packCallingIdentity function in libbinder and use it directly + // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp + private static long packBinderIdentityToken( + boolean hasExplicitIdentity, int callingUid, int callingPid) { + long res = ((long) callingUid << 32) | callingPid; + if (hasExplicitIdentity) { + res |= (0x1 << 30); + } else { + res &= ~(0x1 << 30); + } + return res; + } + public static void init(RavenwoodRule rule) throws IOException { if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) { maybeThrowPendingUncaughtException(false); Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); } - RuntimeInit.redirectLogStreams(); - android.os.Process.init$ravenwood(rule.mUid, rule.mPid); - android.os.Binder.init$ravenwood(); + sOriginalIdentityToken = Binder.clearCallingIdentity(); + Binder.restoreCallingIdentity(packBinderIdentityToken(false, rule.mUid, rule.mPid)); setSystemProperties(rule.mSystemProperties); ServiceManager.init$ravenwood(); @@ -176,7 +190,7 @@ public class RavenwoodRuleImpl { ServiceManager.reset$ravenwood(); setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES); - android.os.Binder.reset$ravenwood(); + Binder.restoreCallingIdentity(sOriginalIdentityToken); android.os.Process.reset$ravenwood(); ResourcesManager.setInstance(null); // Better structure needed. diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java index 631f68ff1dec..3ffabefb7681 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java @@ -127,7 +127,11 @@ public class RavenwoodTestStats { int passed = 0; int skipped = 0; int failed = 0; - for (var e : mStats.get(classDescription).values()) { + var stats = mStats.get(classDescription); + if (stats == null) { + return; + } + for (var e : stats.values()) { switch (e) { case Passed: passed++; break; case Skipped: skipped++; break; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 7d991663f4b1..dffb263e77cb 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -15,21 +15,25 @@ */ package android.platform.test.ravenwood; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod; import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.Log; -import com.android.ravenwood.common.RavenwoodCommonUtils; import com.android.ravenwood.common.SneakyThrow; import org.junit.Assume; +import org.junit.AssumptionViolatedException; import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; import org.junit.rules.TestRule; import org.junit.runner.Description; +import org.junit.runner.Result; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; @@ -40,8 +44,11 @@ import org.junit.runner.manipulation.Orderer; import org.junit.runner.manipulation.Sortable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; import org.junit.runner.notification.RunNotifier; +import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.MultipleFailureException; import org.junit.runners.model.RunnerBuilder; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; @@ -52,6 +59,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Stack; /** * A test runner used for Ravenwood. @@ -62,7 +71,7 @@ import java.lang.reflect.InvocationTargetException; * the inner runner gets a chance to run. This can be used to initialize stuff used by the * inner runner. * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from - * the four test rules such as {@link #sImplicitClassMinRule}, which are also injected by + * the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by * the ravenizer tool. * * We use this runner to: @@ -75,7 +84,7 @@ import java.lang.reflect.InvocationTargetException; * (no hooks, etc.) */ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orderable { - private static final String TAG = "RavenwoodAwareTestRunner"; + public static final String TAG = "Ravenwood"; @Inherited @Target({TYPE}) @@ -103,28 +112,50 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde /** Order of a hook. */ public enum Order { - First, - Last, + Outer, + Inner, } // The following four rule instances will be injected to tests by the Ravenizer tool. + private static class RavenwoodClassOuterRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Outer); + } + } - public static final TestRule sImplicitClassMinRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Class, Order.First); + private static class RavenwoodClassInnerRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Inner); + } + } - public static final TestRule sImplicitClassMaxRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Last); + private static class RavenwoodInstanceOuterRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement( + base, description, Scope.Instance, Order.Outer); + } + } - public static final TestRule sImplicitInstMinRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.First); + private static class RavenwoodInstanceInnerRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement( + base, description, Scope.Instance, Order.Inner); + } + } - public static final TestRule sImplicitInstMaxRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.Last); + public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule(); + public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule(); + public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule(); + public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule(); - public static final String IMPLICIT_CLASS_MIN_RULE_NAME = "sImplicitClassMinRule"; - public static final String IMPLICIT_CLASS_MAX_RULE_NAME = "sImplicitClassMaxRule"; - public static final String IMPLICIT_INST_MIN_RULE_NAME = "sImplicitInstMinRule"; - public static final String IMPLICIT_INST_MAX_RULE_NAME = "sImplicitInstMaxRule"; + public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule"; + public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule"; + public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule"; + public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule"; /** Keeps track of the runner on the current thread. */ private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>(); @@ -142,16 +173,9 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde private Description mDescription = null; private Throwable mExceptionInConstructor = null; - /** Simple logging method. */ - private void log(String message) { - RavenwoodCommonUtils.log(TAG, "[" + getTestClass().getJavaClass() + " @" + this + "] " - + message); - } - - private Error logAndFail(String message, Throwable innerException) { - log(message); - log(" Exception=" + innerException); - throw new AssertionError(message, innerException); + private Error logAndFail(String message, Throwable exception) { + Log.e(TAG, message, exception); + throw new AssertionError(message, exception); } public TestClass getTestClass() { @@ -165,6 +189,10 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde try { mTestClass = new TestClass(testClass); + Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName()); + + onRunnerInitializing(); + /* * If the class has @DisabledOnRavenwood, then we'll delegate to * ClassSkippingTestRunner, which simply skips it. @@ -186,10 +214,8 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde realRunnerClass = BlockJUnit4ClassRunner.class; } - onRunnerInitializing(); - try { - log("Initializing the inner runner: " + realRunnerClass); + Log.i(TAG, "Initializing the inner runner: " + realRunnerClass); mRealRunner = instantiateRealRunner(realRunnerClass, testClass); mDescription = mRealRunner.getDescription(); @@ -201,8 +227,7 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } catch (Throwable th) { // If we throw in the constructor, Tradefed may not report it and just ignore the class, // so record it and throw it when the test actually started. - log("Fatal: Exception detected in constructor: " + th.getMessage() + "\n" - + Log.getStackTraceString(th)); + Log.e(TAG, "Fatal: Exception detected in constructor", th); mExceptionInConstructor = new RuntimeException("Exception detected in constructor", th); mDescription = Description.createTestDescription(testClass, "Constructor"); @@ -236,8 +261,7 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde if (!isOnRavenwood()) { return; } - - log("onRunnerInitializing"); + // DO NOT USE android.util.Log before calling onRunnerInitializing(). RavenwoodAwareTestRunnerHook.onRunnerInitializing(this, mTestClass); @@ -250,7 +274,7 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde if (!isOnRavenwood()) { return; } - log("runAnnotatedMethodsOnRavenwood() " + annotationClass.getName()); + Log.v(TAG, "runAnnotatedMethodsOnRavenwood() " + annotationClass.getName()); for (var method : getTestClass().getAnnotatedMethods(annotationClass)) { ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null); @@ -271,20 +295,27 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } @Override - public void run(RunNotifier notifier) { + public void run(RunNotifier realNotifier) { + final RunNotifier notifier = new RavenwoodRunNotifier(realNotifier); + if (mRealRunner instanceof ClassSkippingTestRunner) { mRealRunner.run(notifier); RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription()); return; } + Log.v(TAG, "Starting " + mTestClass.getJavaClass().getCanonicalName()); + if (RAVENWOOD_VERBOSE_LOGGING) { + dumpDescription(getDescription()); + } + if (maybeReportExceptionFromConstructor(notifier)) { return; } sCurrentRunner.set(this); try { - runWithHooks(getDescription(), Scope.Runner, Order.First, + runWithHooks(getDescription(), Scope.Runner, Order.Outer, () -> mRealRunner.run(notifier)); } finally { sCurrentRunner.remove(); @@ -409,4 +440,217 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } } } + + private void dumpDescription(Description desc) { + dumpDescription(desc, "[TestDescription]=", " "); + } + + private void dumpDescription(Description desc, String header, String indent) { + Log.v(TAG, indent + header + desc); + + var children = desc.getChildren(); + var childrenIndent = " " + indent; + for (int i = 0; i < children.size(); i++) { + dumpDescription(children.get(i), "#" + i + ": ", childrenIndent); + } + } + + /** + * A run notifier that wraps another notifier and provides the following features: + * - Handle a failure that happened before testStarted and testEnded (typically that means + * it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if + * individual tests in the class reported it. This is for b/364395552. + * + * - Logging. + */ + private class RavenwoodRunNotifier extends RunNotifier { + private final RunNotifier mRealNotifier; + + private final Stack<Description> mSuiteStack = new Stack<>(); + private Description mCurrentSuite = null; + private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>(); + + private boolean mBeforeTest = true; + private boolean mAfterTest = false; + + private RavenwoodRunNotifier(RunNotifier realNotifier) { + mRealNotifier = realNotifier; + } + + private boolean isInTest() { + return !mBeforeTest && !mAfterTest; + } + + @Override + public void addListener(RunListener listener) { + mRealNotifier.addListener(listener); + } + + @Override + public void removeListener(RunListener listener) { + mRealNotifier.removeListener(listener); + } + + @Override + public void addFirstListener(RunListener listener) { + mRealNotifier.addFirstListener(listener); + } + + @Override + public void fireTestRunStarted(Description description) { + Log.i(TAG, "testRunStarted: " + description); + mRealNotifier.fireTestRunStarted(description); + } + + @Override + public void fireTestRunFinished(Result result) { + Log.i(TAG, "testRunFinished: " + + result.getRunCount() + "," + + result.getFailureCount() + "," + + result.getAssumptionFailureCount() + "," + + result.getIgnoreCount()); + mRealNotifier.fireTestRunFinished(result); + } + + @Override + public void fireTestSuiteStarted(Description description) { + Log.i(TAG, "testSuiteStarted: " + description); + mRealNotifier.fireTestSuiteStarted(description); + + mBeforeTest = true; + mAfterTest = false; + + // Keep track of the current suite, needed if the outer test is a Suite, + // in which case its children are test classes. (not test methods) + mCurrentSuite = description; + mSuiteStack.push(description); + + mOutOfTestFailures.clear(); + } + + @Override + public void fireTestSuiteFinished(Description description) { + Log.i(TAG, "testSuiteFinished: " + description); + mRealNotifier.fireTestSuiteFinished(description); + + maybeHandleOutOfTestFailures(); + + mBeforeTest = true; + mAfterTest = false; + + // Restore the upper suite. + mSuiteStack.pop(); + mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek(); + } + + @Override + public void fireTestStarted(Description description) throws StoppedByUserException { + Log.i(TAG, "testStarted: " + description); + mRealNotifier.fireTestStarted(description); + + mAfterTest = false; + mBeforeTest = false; + } + + @Override + public void fireTestFailure(Failure failure) { + Log.i(TAG, "testFailure: " + failure); + + if (isInTest()) { + mRealNotifier.fireTestFailure(failure); + } else { + mOutOfTestFailures.add(failure.getException()); + } + } + + @Override + public void fireTestAssumptionFailed(Failure failure) { + Log.i(TAG, "testAssumptionFailed: " + failure); + + if (isInTest()) { + mRealNotifier.fireTestAssumptionFailed(failure); + } else { + mOutOfTestFailures.add(failure.getException()); + } + } + + @Override + public void fireTestIgnored(Description description) { + Log.i(TAG, "testIgnored: " + description); + mRealNotifier.fireTestIgnored(description); + } + + @Override + public void fireTestFinished(Description description) { + Log.i(TAG, "testFinished: " + description); + mRealNotifier.fireTestFinished(description); + + mAfterTest = true; + } + + @Override + public void pleaseStop() { + Log.w(TAG, "pleaseStop:"); + mRealNotifier.pleaseStop(); + } + + /** + * At the end of each Suite, we handle failures happened out of test methods. + * (typically in @BeforeClass or @AfterClasses) + * + * This is to work around b/364395552. + */ + private boolean maybeHandleOutOfTestFailures() { + if (mOutOfTestFailures.size() == 0) { + return false; + } + Throwable th; + if (mOutOfTestFailures.size() == 1) { + th = mOutOfTestFailures.get(0); + } else { + th = new MultipleFailureException(mOutOfTestFailures); + } + if (mBeforeTest) { + reportBeforeTestFailure(mCurrentSuite, th); + return true; + } + if (mAfterTest) { + // Unfortunately, there's no good way to report it, so kill the own process. + onCriticalError( + "Failures detected in @AfterClass, which would be swalloed by tradefed", + th); + return true; // unreachable + } + return false; + } + + private void reportBeforeTestFailure(Description suiteDesc, Throwable th) { + // If a failure happens befere running any tests, we'll need to pretend + // as if each test in the suite reported the failure, to work around b/364395552. + for (var child : suiteDesc.getChildren()) { + if (child.isSuite()) { + // If the chiil is still a "parent" -- a test class or a test suite + // -- propagate to its children. + mRealNotifier.fireTestSuiteStarted(child); + reportBeforeTestFailure(child, th); + mRealNotifier.fireTestSuiteFinished(child); + } else { + mRealNotifier.fireTestStarted(child); + Failure f = new Failure(child, th); + if (th instanceof AssumptionViolatedException) { + mRealNotifier.fireTestAssumptionFailed(f); + } else { + mRealNotifier.fireTestFailure(f); + } + mRealNotifier.fireTestFinished(child); + } + } + } + } + + private void onCriticalError(@NonNull String message, @Nullable Throwable th) { + Log.e(TAG, "Critical error! Ravenwood cannot continue. Killing self process: " + + message, th); + System.exit(1); + } } diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index 7b5bc5aeb7b6..875ce71149cd 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -15,12 +15,17 @@ */ package com.android.ravenwood.common; +import android.annotation.NonNull; +import android.annotation.Nullable; + import com.android.ravenwood.common.divergence.RavenwoodDivergence; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -33,6 +38,14 @@ public class RavenwoodCommonUtils { private static final Object sLock = new Object(); + /** + * If set to "1", we enable the verbose logging. + * + * (See also InitLogging() in http://ac/system/libbase/logging.cpp) + */ + public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv( + "RAVENWOOD_VERBOSE")); + /** Name of `libravenwood_runtime` */ private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime"; @@ -265,4 +278,12 @@ public class RavenwoodCommonUtils { method.getDeclaringClass().getName(), method.getName(), (isStatic ? "static " : ""))); } + + @NonNull + public static String getStackTraceString(@Nullable Throwable th) { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + th.printStackTrace(writer); + return stringWriter.toString(); + } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java index f38d5653d3a9..e21a9cd71a2d 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java +++ b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package android.database; -import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.os.Parcel; import android.util.Base64; @@ -35,8 +34,8 @@ public class CursorWindow_host { private String mName; private int mColumnNum; private static class Row { - String[] fields; - int[] types; + String[] mFields; + int[] mTypes; } private final List<Row> mRows = new ArrayList<>(); @@ -69,9 +68,9 @@ public class CursorWindow_host { public static boolean nativeAllocRow(long windowPtr) { CursorWindow_host instance = sInstances.get(windowPtr); Row row = new Row(); - row.fields = new String[instance.mColumnNum]; - row.types = new int[instance.mColumnNum]; - Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL); + row.mFields = new String[instance.mColumnNum]; + row.mTypes = new int[instance.mColumnNum]; + Arrays.fill(row.mTypes, Cursor.FIELD_TYPE_NULL); instance.mRows.add(row); return true; } @@ -82,8 +81,8 @@ public class CursorWindow_host { return false; } Row r = instance.mRows.get(row); - r.fields[column] = value; - r.types[column] = type; + r.mFields[column] = value; + r.mTypes[column] = type; return true; } @@ -93,7 +92,7 @@ public class CursorWindow_host { return Cursor.FIELD_TYPE_NULL; } - return instance.mRows.get(row).types[column]; + return instance.mRows.get(row).mTypes[column]; } public static boolean nativePutString(long windowPtr, String value, @@ -107,7 +106,7 @@ public class CursorWindow_host { return null; } - return instance.mRows.get(row).fields[column]; + return instance.mRows.get(row).mFields[column]; } public static boolean nativePutLong(long windowPtr, long value, int row, int column) { @@ -170,8 +169,8 @@ public class CursorWindow_host { parcel.writeInt(window.mColumnNum); parcel.writeInt(window.mRows.size()); for (int row = 0; row < window.mRows.size(); row++) { - parcel.writeStringArray(window.mRows.get(row).fields); - parcel.writeIntArray(window.mRows.get(row).types); + parcel.writeStringArray(window.mRows.get(row).mFields); + parcel.writeIntArray(window.mRows.get(row).mTypes); } } @@ -183,8 +182,8 @@ public class CursorWindow_host { int rowCount = parcel.readInt(); for (int row = 0; row < rowCount; row++) { Row r = new Row(); - r.fields = parcel.createStringArray(); - r.types = parcel.createIntArray(); + r.mFields = parcel.createStringArray(); + r.mTypes = parcel.createIntArray(); window.mRows.add(r); } return windowPtr; diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java index 5e81124b6e70..1b63adc4319f 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java +++ b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package android.os; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java index e7479d313918..b09bf3119cfa 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java +++ b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package android.os; import android.util.SparseArray; @@ -36,9 +36,6 @@ public class SystemProperties_host { /** Predicate tested to determine if a given key can be written. */ @GuardedBy("sLock") private static Predicate<String> sKeyWritablePredicate; - /** Callback to trigger when values are changed */ - @GuardedBy("sLock") - private static Runnable sChangeCallback; /** * Reverse mapping that provides a way back to an original key from the @@ -48,7 +45,7 @@ public class SystemProperties_host { private static SparseArray<String> sKeyHandles = new SparseArray<>(); /** - * Basically the same as {@link #native_init$ravenwood}, but it'll only run if no values are + * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are * set yet. */ public static void initializeIfNeeded(Map<String, String> values, @@ -57,30 +54,32 @@ public class SystemProperties_host { if (sValues != null) { return; // Already initialized. } - native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate, - () -> {}); + init$ravenwood(values, keyReadablePredicate, keyWritablePredicate); } } - public static void native_init$ravenwood(Map<String, String> values, - Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate, - Runnable changeCallback) { + public static void init$ravenwood(Map<String, String> values, + Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) { synchronized (sLock) { sValues = Objects.requireNonNull(values); sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate); sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate); - sChangeCallback = Objects.requireNonNull(changeCallback); sKeyHandles.clear(); + synchronized (SystemProperties.sChangeCallbacks) { + SystemProperties.sChangeCallbacks.clear(); + } } } - public static void native_reset$ravenwood() { + public static void reset$ravenwood() { synchronized (sLock) { sValues = null; sKeyReadablePredicate = null; sKeyWritablePredicate = null; - sChangeCallback = null; sKeyHandles.clear(); + synchronized (SystemProperties.sChangeCallbacks) { + SystemProperties.sChangeCallbacks.clear(); + } } } @@ -101,7 +100,7 @@ public class SystemProperties_host { } else { sValues.put(key, val); } - sChangeCallback.run(); + SystemProperties.callChangeCallbacks(); } } @@ -183,7 +182,7 @@ public class SystemProperties_host { // Report through callback always registered via init above synchronized (sLock) { Preconditions.requireNonNullViaRavenwoodRule(sValues); - sChangeCallback.run(); + SystemProperties.callChangeCallbacks(); } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java index 55d4ffb41e78..878a0ff57a1d 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java +++ b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java @@ -13,12 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package android.util; import com.android.internal.os.RuntimeInit; import java.io.PrintStream; -import java.util.Collection; public class EventLog_host { public static int writeEvent(int tag, int value) { @@ -58,15 +57,6 @@ public class EventLog_host { return sb.length(); } - public static void readEvents(int[] tags, Collection<android.util.EventLog.Event> output) { - throw new UnsupportedOperationException(); - } - - public static void readEventsOnWrapping(int[] tags, long timestamp, - Collection<android.util.EventLog.Event> output) { - throw new UnsupportedOperationException(); - } - /** * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so * that we don't end up in a recursive loop. diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java index f301b9c46b0e..d232ef2076be 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java +++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java @@ -13,9 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package android.util; -import android.util.Log; import android.util.Log.Level; import com.android.internal.os.RuntimeInit; @@ -44,7 +43,7 @@ public class Log_host { case Log.LOG_ID_SYSTEM: buffer = "system"; break; case Log.LOG_ID_CRASH: buffer = "crash"; break; default: buffer = "buf:" + bufID; break; - }; + } final String prio; switch (priority) { @@ -55,7 +54,7 @@ public class Log_host { case Log.ERROR: prio = "E"; break; case Log.ASSERT: prio = "A"; break; default: prio = "prio:" + priority; break; - }; + } for (String s : msg.split("\\n")) { getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s)); diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java new file mode 100644 index 000000000000..c18c307ad1e3 --- /dev/null +++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open 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.os; + +import java.util.Arrays; +import java.util.HashMap; + +public class LongArrayContainer_host { + private static final HashMap<Long, long[]> sInstances = new HashMap<>(); + private static long sNextId = 1; + + public static long native_init(int arrayLength) { + long[] array = new long[arrayLength]; + long instanceId = sNextId++; + sInstances.put(instanceId, array); + return instanceId; + } + + static long[] getInstance(long instanceId) { + return sInstances.get(instanceId); + } + + public static void native_setValues(long instanceId, long[] values) { + System.arraycopy(values, 0, getInstance(instanceId), 0, values.length); + } + + public static void native_getValues(long instanceId, long[] values) { + System.arraycopy(getInstance(instanceId), 0, values, 0, values.length); + } + + public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) { + long[] values = getInstance(instanceId); + + boolean nonZero = false; + Arrays.fill(array, 0); + + for (int i = 0; i < values.length; i++) { + int index = indexMap[i]; + if (index < 0 || index >= array.length) { + throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, " + + (array.length - 1) + "]"); + } + if (values[i] != 0) { + array[index] += values[i]; + nonZero = true; + } + } + return nonZero; + } +} diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java index 0f65544f8b66..9ce8ea8e16ef 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package com.android.internal.os; import android.os.BadParcelableException; import android.os.Parcel; @@ -28,7 +28,7 @@ import java.util.HashMap; public class LongArrayMultiStateCounter_host { /** - * A reimplementation of {@link com.android.internal.os.LongArrayMultiStateCounter}, only in + * A reimplementation of {@link LongArrayMultiStateCounter}, only in * Java instead of native. The majority of the code (in C++) can be found in * /frameworks/native/libs/battery/MultiStateCounter.h */ @@ -257,50 +257,6 @@ public class LongArrayMultiStateCounter_host { } } - public static class LongArrayContainer_host { - private static final HashMap<Long, long[]> sInstances = new HashMap<>(); - private static long sNextId = 1; - - public static long native_init(int arrayLength) { - long[] array = new long[arrayLength]; - long instanceId = sNextId++; - sInstances.put(instanceId, array); - return instanceId; - } - - static long[] getInstance(long instanceId) { - return sInstances.get(instanceId); - } - - public static void native_setValues(long instanceId, long[] values) { - System.arraycopy(values, 0, getInstance(instanceId), 0, values.length); - } - - public static void native_getValues(long instanceId, long[] values) { - System.arraycopy(getInstance(instanceId), 0, values, 0, values.length); - } - - public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) { - long[] values = getInstance(instanceId); - - boolean nonZero = false; - Arrays.fill(array, 0); - - for (int i = 0; i < values.length; i++) { - int index = indexMap[i]; - if (index < 0 || index >= array.length) { - throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, " - + (array.length - 1) + "]"); - } - if (values[i] != 0) { - array[index] += values[i]; - nonZero = true; - } - } - return nonZero; - } - } - private static final HashMap<Long, LongArrayMultiStateCounterRavenwood> sInstances = new HashMap<>(); private static long sNextId = 1; diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java index 9486651ce48d..1d95aa143549 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package com.android.internal.os; import android.os.BadParcelableException; import android.os.Parcel; diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java index 58f6bbb5baf5..3bf116da19b8 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.platform.test.ravenwood.nativesubstitution; +package com.android.internal.ravenwood; +import android.os.SystemProperties_host; import android.platform.test.ravenwood.RavenwoodSystemProperties; import android.util.Log; -import com.android.internal.ravenwood.RavenwoodEnvironment; import com.android.ravenwood.common.JvmWorkaround; import com.android.ravenwood.common.RavenwoodCommonUtils; @@ -36,7 +36,7 @@ public class RavenwoodEnvironment_host { /** * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}. */ - public static void nativeEnsureRavenwoodInitialized() { + public static void ensureRavenwoodInitialized() { // TODO Unify it with the initialization code in RavenwoodAwareTestRunnerHook. @@ -63,14 +63,14 @@ public class RavenwoodEnvironment_host { /** * Called from {@link RavenwoodEnvironment#getRavenwoodRuntimePath()}. */ - public static String nativeGetRavenwoodRuntimePath(RavenwoodEnvironment env) { + public static String getRavenwoodRuntimePath(RavenwoodEnvironment env) { return RavenwoodCommonUtils.getRavenwoodRuntimePath(); } /** * Called from {@link RavenwoodEnvironment#fromAddress(long)}. */ - public static <T> T nativeFromAddress(RavenwoodEnvironment env, long address) { + public static <T> T fromAddress(RavenwoodEnvironment env, long address) { return JvmWorkaround.getInstance().fromAddress(address); } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java deleted file mode 100644 index cb00b3e758fa..000000000000 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java +++ /dev/null @@ -1,530 +0,0 @@ -/* - * 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.platform.test.ravenwood.nativesubstitution; - -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; - -import java.io.FileDescriptor; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Tentative, partial implementation of the Parcel native methods, using Java's - * {@code byte[]}. - * (We don't use a {@link ByteBuffer} because there's enough semantics differences between Parcel - * and {@link ByteBuffer}, and it didn't work out. - * e.g. Parcel seems to allow moving the data position to be beyond its size? Which - * {@link ByteBuffer} wouldn't allow...) - */ -public class Parcel_host { - private static final String TAG = "Parcel"; - - private Parcel_host() { - } - - private static final AtomicLong sNextId = new AtomicLong(1); - - private static final Map<Long, Parcel_host> sInstances = new ConcurrentHashMap<>(); - - private boolean mDeleted = false; - - private byte[] mBuffer; - private int mSize; - private int mPos; - - private boolean mSensitive; - private boolean mAllowFds; - - // TODO Use the actual value from Parcel.java. - private static final int OK = 0; - - private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>(); - - private static final int FD_PLACEHOLDER = 0xDEADBEEF; - private static final int FD_PAYLOAD_SIZE = 8; - - private void validate() { - if (mDeleted) { - // TODO: Put more info - throw new RuntimeException("Parcel already destroyed"); - } - } - - private static Parcel_host getInstance(long id) { - Parcel_host p = sInstances.get(id); - if (p == null) { - // TODO: Put more info - throw new RuntimeException("Parcel doesn't exist with id=" + id); - } - p.validate(); - return p; - } - - /** Native method substitution */ - public static long nativeCreate() { - final long id = sNextId.getAndIncrement(); - final Parcel_host p = new Parcel_host(); - sInstances.put(id, p); - p.init(); - return id; - } - - private void init() { - mBuffer = new byte[0]; - mSize = 0; - mPos = 0; - mSensitive = false; - mAllowFds = true; - mFdMap.clear(); - } - - private void updateSize() { - if (mSize < mPos) { - mSize = mPos; - } - } - - /** Native method substitution */ - public static void nativeDestroy(long nativePtr) { - getInstance(nativePtr).mDeleted = true; - sInstances.remove(nativePtr); - } - - /** Native method substitution */ - public static void nativeFreeBuffer(long nativePtr) { - getInstance(nativePtr).freeBuffer(); - } - - /** Native method substitution */ - private void freeBuffer() { - init(); - } - - private int getCapacity() { - return mBuffer.length; - } - - private void ensureMoreCapacity(int size) { - ensureCapacity(mPos + size); - } - - private void ensureCapacity(int targetSize) { - if (targetSize <= getCapacity()) { - return; - } - var newSize = getCapacity() * 2; - if (newSize < targetSize) { - newSize = targetSize; - } - forceSetCapacity(newSize); - } - - private void forceSetCapacity(int newSize) { - var newBuf = new byte[newSize]; - - // Copy - System.arraycopy(mBuffer, 0, newBuf, 0, Math.min(newSize, getCapacity())); - - this.mBuffer = newBuf; - } - - private void ensureDataAvailable(int requestSize) { - if (mSize - mPos < requestSize) { - throw new RuntimeException(String.format( - "Pacel data underflow. size=%d, pos=%d, request=%d", mSize, mPos, requestSize)); - } - } - - /** Native method substitution */ - public static void nativeMarkSensitive(long nativePtr) { - getInstance(nativePtr).mSensitive = true; - } - - /** Native method substitution */ - public static int nativeDataSize(long nativePtr) { - return getInstance(nativePtr).mSize; - } - - /** Native method substitution */ - public static int nativeDataAvail(long nativePtr) { - var p = getInstance(nativePtr); - return p.mSize - p.mPos; - } - - /** Native method substitution */ - public static int nativeDataPosition(long nativePtr) { - return getInstance(nativePtr).mPos; - } - - /** Native method substitution */ - public static int nativeDataCapacity(long nativePtr) { - return getInstance(nativePtr).mBuffer.length; - } - - /** Native method substitution */ - public static void nativeSetDataSize(long nativePtr, int size) { - var p = getInstance(nativePtr); - p.ensureCapacity(size); - getInstance(nativePtr).mSize = size; - } - - /** Native method substitution */ - public static void nativeSetDataPosition(long nativePtr, int pos) { - var p = getInstance(nativePtr); - // TODO: Should this change the size or the capacity?? - p.mPos = pos; - } - - /** Native method substitution */ - public static void nativeSetDataCapacity(long nativePtr, int size) { - if (size < 0) { - throw new IllegalArgumentException("size < 0: size=" + size); - } - var p = getInstance(nativePtr); - if (p.getCapacity() < size) { - p.forceSetCapacity(size); - } - } - - /** Native method substitution */ - public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) { - var p = getInstance(nativePtr); - var prev = p.mAllowFds; - p.mAllowFds = allowFds; - return prev; - } - - /** Native method substitution */ - public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) { - getInstance(nativePtr).mAllowFds = lastValue; - } - - /** Native method substitution */ - public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) { - nativeWriteBlob(nativePtr, b, offset, len); - } - - /** Native method substitution */ - public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) { - var p = getInstance(nativePtr); - - if (b == null) { - nativeWriteInt(nativePtr, -1); - } else { - final var alignedSize = align4(len); - - nativeWriteInt(nativePtr, len); - - p.ensureMoreCapacity(alignedSize); - - System.arraycopy(b, offset, p.mBuffer, p.mPos, len); - p.mPos += alignedSize; - p.updateSize(); - } - } - - /** Native method substitution */ - public static int nativeWriteInt(long nativePtr, int value) { - var p = getInstance(nativePtr); - p.ensureMoreCapacity(Integer.BYTES); - - p.mBuffer[p.mPos++] = (byte) ((value >> 24) & 0xff); - p.mBuffer[p.mPos++] = (byte) ((value >> 16) & 0xff); - p.mBuffer[p.mPos++] = (byte) ((value >> 8) & 0xff); - p.mBuffer[p.mPos++] = (byte) ((value >> 0) & 0xff); - - p.updateSize(); - - return OK; - } - - /** Native method substitution */ - public static int nativeWriteLong(long nativePtr, long value) { - nativeWriteInt(nativePtr, (int) (value >>> 32)); - nativeWriteInt(nativePtr, (int) (value)); - return OK; - } - - /** Native method substitution */ - public static int nativeWriteFloat(long nativePtr, float val) { - return nativeWriteInt(nativePtr, Float.floatToIntBits(val)); - } - - /** Native method substitution */ - public static int nativeWriteDouble(long nativePtr, double val) { - return nativeWriteLong(nativePtr, Double.doubleToLongBits(val)); - } - - private static int align4(int val) { - return ((val + 3) / 4) * 4; - } - - /** Native method substitution */ - public static void nativeWriteString8(long nativePtr, String val) { - if (val == null) { - nativeWriteBlob(nativePtr, null, 0, 0); - } else { - var bytes = val.getBytes(StandardCharsets.UTF_8); - nativeWriteBlob(nativePtr, bytes, 0, bytes.length); - } - } - - /** Native method substitution */ - public static void nativeWriteString16(long nativePtr, String val) { - // Just reuse String8 - nativeWriteString8(nativePtr, val); - } - - /** Native method substitution */ - public static byte[] nativeCreateByteArray(long nativePtr) { - return nativeReadBlob(nativePtr); - } - - /** Native method substitution */ - public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) { - if (dest == null) { - return false; - } - var data = nativeReadBlob(nativePtr); - if (data == null) { - System.err.println("Percel has NULL, which is unexpected."); // TODO: Is this correct? - return false; - } - // TODO: Make sure the check logic is correct. - if (data.length != destLen) { - System.err.println("Byte array size mismatch: expected=" - + data.length + " given=" + destLen); - return false; - } - System.arraycopy(data, 0, dest, 0, data.length); - return true; - } - - /** Native method substitution */ - public static byte[] nativeReadBlob(long nativePtr) { - var p = getInstance(nativePtr); - if (p.mSize - p.mPos < 4) { - // Match native impl that returns "null" when not enough data - return null; - } - final var size = nativeReadInt(nativePtr); - if (size == -1) { - return null; - } - try { - p.ensureDataAvailable(align4(size)); - } catch (Exception e) { - System.err.println(e.toString()); - return null; - } - - var bytes = new byte[size]; - System.arraycopy(p.mBuffer, p.mPos, bytes, 0, size); - - p.mPos += align4(size); - - return bytes; - } - - /** Native method substitution */ - public static int nativeReadInt(long nativePtr) { - var p = getInstance(nativePtr); - - if (p.mSize - p.mPos < 4) { - // Match native impl that returns "0" when not enough data - return 0; - } - - var ret = (((p.mBuffer[p.mPos++] & 0xff) << 24) - | ((p.mBuffer[p.mPos++] & 0xff) << 16) - | ((p.mBuffer[p.mPos++] & 0xff) << 8) - | ((p.mBuffer[p.mPos++] & 0xff) << 0)); - - return ret; - } - - /** Native method substitution */ - public static long nativeReadLong(long nativePtr) { - return (((long) nativeReadInt(nativePtr)) << 32) - | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL); - } - - /** Native method substitution */ - public static float nativeReadFloat(long nativePtr) { - return Float.intBitsToFloat(nativeReadInt(nativePtr)); - } - - /** Native method substitution */ - public static double nativeReadDouble(long nativePtr) { - return Double.longBitsToDouble(nativeReadLong(nativePtr)); - } - - /** Native method substitution */ - public static String nativeReadString8(long nativePtr) { - final var bytes = nativeReadBlob(nativePtr); - if (bytes == null) { - return null; - } - return new String(bytes, StandardCharsets.UTF_8); - } - public static String nativeReadString16(long nativePtr) { - return nativeReadString8(nativePtr); - } - - /** Native method substitution */ - public static byte[] nativeMarshall(long nativePtr) { - var p = getInstance(nativePtr); - return Arrays.copyOf(p.mBuffer, p.mSize); - } - - /** Native method substitution */ - public static void nativeUnmarshall( - long nativePtr, byte[] data, int offset, int length) { - var p = getInstance(nativePtr); - p.ensureMoreCapacity(length); - System.arraycopy(data, offset, p.mBuffer, p.mPos, length); - p.mPos += length; - p.updateSize(); - } - - /** Native method substitution */ - public static int nativeCompareData(long thisNativePtr, long otherNativePtr) { - var a = getInstance(thisNativePtr); - var b = getInstance(otherNativePtr); - if ((a.mSize == b.mSize) && Arrays.equals(a.mBuffer, b.mBuffer)) { - return 0; - } else { - return -1; - } - } - - /** Native method substitution */ - public static boolean nativeCompareDataInRange( - long ptrA, int offsetA, long ptrB, int offsetB, int length) { - var a = getInstance(ptrA); - var b = getInstance(ptrB); - if (offsetA < 0 || offsetA + length > a.mSize) { - throw new IllegalArgumentException(); - } - if (offsetB < 0 || offsetB + length > b.mSize) { - throw new IllegalArgumentException(); - } - return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length), - Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length)); - } - - /** Native method substitution */ - public static void nativeAppendFrom( - long thisNativePtr, long otherNativePtr, int srcOffset, int length) { - var dst = getInstance(thisNativePtr); - var src = getInstance(otherNativePtr); - - dst.ensureMoreCapacity(length); - - System.arraycopy(src.mBuffer, srcOffset, dst.mBuffer, dst.mPos, length); - dst.mPos += length; // TODO: 4 byte align? - dst.updateSize(); - - // TODO: Update the other's position? - } - - /** Native method substitution */ - public static boolean nativeHasBinders(long nativePtr) { - // Assume false for now, because we don't support adding binders. - return false; - } - - /** Native method substitution */ - public static boolean nativeHasBindersInRange( - long nativePtr, int offset, int length) { - // Assume false for now, because we don't support writing FDs yet. - return false; - } - - /** Native method substitution */ - public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) { - var p = getInstance(nativePtr); - - if (!p.mAllowFds) { - // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp - throw new RuntimeException("Not allowed to write file descriptors here"); - } - - FileDescriptor dup = null; - try { - dup = Os.dup(val); - } catch (ErrnoException e) { - throw new RuntimeException(e); - } - p.mFdMap.put(p.mPos, dup); - - // Parcel.cpp writes two int32s for a FD. - // Make sure FD_PAYLOAD_SIZE is in sync with this code. - nativeWriteInt(nativePtr, FD_PLACEHOLDER); - nativeWriteInt(nativePtr, FD_PLACEHOLDER); - } - - /** Native method substitution */ - public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) { - var p = getInstance(nativePtr); - - var pos = p.mPos; - var fd = p.mFdMap.get(pos); - - if (fd == null) { - Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos); - return null; - } - nativeReadInt(nativePtr); - nativeReadInt(nativePtr); - return fd; - } - - /** Native method substitution */ - public static boolean nativeHasFileDescriptors(long nativePtr) { - var p = getInstance(nativePtr); - return p.mFdMap.size() > 0; - } - - /** Native method substitution */ - public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) { - var p = getInstance(nativePtr); - - // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp - if (offset < 0 || length < 0) { - throw new IllegalArgumentException("Negative value not allowed: offset=" + offset - + " length=" + length); - } - long limit = (long) offset + (long) length; - if (limit > p.mSize) { - throw new IllegalArgumentException("Out of range: offset=" + offset - + " length=" + length + " dataSize=" + p.mSize); - } - - for (var pos : p.mFdMap.keySet()) { - if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) { - return true; - } - } - return false; - } -}
\ No newline at end of file diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java index 0f955e772445..790bb1c2373b 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java @@ -15,6 +15,11 @@ */ package com.android.platform.test.ravenwood.runtimehelper; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; + +import android.system.ErrnoException; +import android.system.Os; + import com.android.ravenwood.common.RavenwoodCommonUtils; import java.io.File; @@ -123,6 +128,15 @@ public class ClassLoadHook { return; } + if (RAVENWOOD_VERBOSE_LOGGING) { + log("Force enabling verbose logging"); + try { + Os.setenv("ANDROID_LOG_TAGS", "*:v", true); + } catch (ErrnoException e) { + // Shouldn't happen. + } + } + // Make sure these properties are not set. ensurePropertyNotSet(CORE_NATIVE_CLASSES); ensurePropertyNotSet(ICU_DATA_PATH); @@ -152,6 +166,7 @@ public class ClassLoadHook { private static final Class<?>[] sLibandroidClasses = { android.util.Log.class, android.os.Parcel.class, + android.os.Binder.class, android.content.res.ApkAssets.class, android.content.res.AssetManager.class, android.content.res.StringBlock.class, diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java index a5c0b54a8637..c94ef31a5e5e 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java +++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java @@ -93,4 +93,8 @@ public final class Os { throw new ErrnoException("pread", OsConstants.EIO, e); } } + + public static void setenv(String name, String value, boolean overwrite) throws ErrnoException { + RavenwoodRuntimeNative.setenv(name, value, overwrite); + } } diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java index 0d8408c12033..ad80d92686ab 100644 --- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java +++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java @@ -53,6 +53,9 @@ public class RavenwoodRuntimeNative { private static native int nOpen(String path, int flags, int mode) throws ErrnoException; + public static native void setenv(String name, String value, boolean overwrite) + throws ErrnoException; + public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException { return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence); } diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp index f5cb019f4e7e..c255be5f61aa 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -214,6 +214,19 @@ static jint Linux_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint return throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode))); } +static void Linux_setenv(JNIEnv* env, jobject, jstring javaName, jstring javaValue, + jboolean overwrite) { + ScopedRealUtf8Chars name(env, javaName); + if (name.c_str() == NULL) { + jniThrowNullPointerException(env); + } + ScopedRealUtf8Chars value(env, javaValue); + if (value.c_str() == NULL) { + jniThrowNullPointerException(env); + } + throwIfMinusOne(env, "setenv", setenv(name.c_str(), value.c_str(), overwrite ? 1 : 0)); +} + // ---- Registration ---- static const JNINativeMethod sMethods[] = @@ -227,6 +240,7 @@ static const JNINativeMethod sMethods[] = { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat }, { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat }, { "nOpen", "(Ljava/lang/String;II)I", (void*)Linux_open }, + { "setenv", "(Ljava/lang/String;Ljava/lang/String;Z)V", (void*)Linux_setenv }, }; extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt index 952ab8244e64..3ec3e3ce2946 100644 --- a/ravenwood/texts/ravenwood-standard-options.txt +++ b/ravenwood/texts/ravenwood-standard-options.txt @@ -32,8 +32,11 @@ --substitute-annotation android.ravenwood.annotation.RavenwoodReplace ---native-substitute-annotation - android.ravenwood.annotation.RavenwoodNativeSubstitutionClass +--redirect-annotation + android.ravenwood.annotation.RavenwoodRedirect + +--redirection-class-annotation + android.ravenwood.annotation.RavenwoodRedirectionClass --class-load-hook-annotation android.ravenwood.annotation.RavenwoodClassLoadHook diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt index eaef2cf6a956..bd9d96d81604 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt @@ -302,7 +302,7 @@ class RunnerRewritingAdapter private constructor( override fun visitCode() { visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_CLASS_MIN_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTSTATIC, @@ -313,7 +313,7 @@ class RunnerRewritingAdapter private constructor( visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_CLASS_MAX_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTSTATIC, @@ -361,7 +361,7 @@ class RunnerRewritingAdapter private constructor( visitVarInsn(ALOAD, 0) visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_INST_MIN_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTFIELD, @@ -373,7 +373,7 @@ class RunnerRewritingAdapter private constructor( visitVarInsn(ALOAD, 0) visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_INST_MAX_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTFIELD, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index b541345e1cb8..7580b697b516 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -58,6 +58,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP; +import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE; import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logAccessibilityShortcutActivated; import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted; import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; @@ -1616,7 +1617,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub /** * Invoked remotely over AIDL by SysUi when the accessibility button within the system's - * navigation area has been clicked. + * navigation area has been clicked, or a gesture shortcut input has been performed. * * @param displayId The logical display id. * @param targetName The flattened {@link ComponentName} string or the class name of a system @@ -1646,7 +1647,26 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } mMainHandler.sendMessage(obtainMessage( AccessibilityManagerService::performAccessibilityShortcutInternal, this, - displayId, SOFTWARE, targetName)); + displayId, getShortcutTypeForGenericShortcutCalls(currentUserId), targetName)); + } + + /** + * AIDL-exposed method to show the dialog + * for choosing the target for the gesture or button shortcuts. + * The shortcut is determined by the current navigation mode. + * + * @param displayId The id for the display to show the dialog on. + */ + @Override + @EnforcePermission(STATUS_BAR_SERVICE) + public void notifyAccessibilityButtonLongClicked(int displayId) { + notifyAccessibilityButtonLongClicked_enforcePermission(); + int userId; + synchronized (mLock) { + userId = mCurrentUserId; + } + showAccessibilityTargetsSelection(displayId, + getShortcutTypeForGenericShortcutCalls(userId), userId); } /** @@ -2344,16 +2364,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private void showAccessibilityTargetsSelection(int displayId, - @UserShortcutType int shortcutType) { + private void showAccessibilityTargetsSelection(int displayId, int shortcutType, + int userId) { final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); final String chooserClassName = (shortcutType == HARDWARE) ? AccessibilityShortcutChooserActivity.class.getName() : AccessibilityButtonChooserActivity.class.getName(); intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra(EXTRA_TYPE_TO_CHOOSE, shortcutType); final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(); - mContext.startActivityAsUser(intent, bundle, UserHandle.of(mCurrentUserId)); + mMainHandler.post(() -> + mContext.startActivityAsUser(intent, bundle, UserHandle.of(userId))); } private void launchShortcutTargetActivity(int displayId, ComponentName name) { @@ -4011,7 +4033,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub * Perform the accessibility shortcut action. * * @param shortcutType The shortcut type. - * @param displayId The display id of the accessibility button. + * @param displayId The display id the shortcut is being performed from. * @param targetName The flattened {@link ComponentName} string or the class name of a system * class implementing a supported accessibility feature, or {@code null} if there's no * specified target. @@ -4031,7 +4053,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (targetName == null) { // In case there are many targets assigned to the given shortcut. if (shortcutTargets.size() > 1) { - showAccessibilityTargetsSelection(displayId, shortcutType); + showAccessibilityTargetsSelection( + displayId, shortcutType, getCurrentUserState().mUserId); return; } targetName = shortcutTargets.get(0); @@ -6563,6 +6586,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub callback)); } + @VisibleForTesting + int getShortcutTypeForGenericShortcutCalls(int userId) { + int navigationMode = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.NAVIGATION_MODE, -1, userId); + if (android.provider.Flags.a11yStandaloneGestureEnabled()) { + return (navigationMode == NAV_BAR_MODE_GESTURAL) ? GESTURE : SOFTWARE; + } else { + return SOFTWARE; + } + } + void attachAccessibilityOverlayToDisplayInternal( int interactionId, int displayId, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index b18e6ba8444d..0bf7ec001d4d 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -57,6 +57,7 @@ import android.view.accessibility.IAccessibilityManagerClient; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.util.ShortcutUtils; import java.io.FileDescriptor; @@ -707,11 +708,16 @@ class AccessibilityUserState { } /** - * Returns true if navibar magnification or shortcut key magnification is enabled. + * Returns true if a magnification shortcut of any type is enabled. */ public boolean isShortcutMagnificationEnabledLocked() { - return mAccessibilityShortcutKeyTargets.contains(MAGNIFICATION_CONTROLLER_NAME) - || mAccessibilityButtonTargets.contains(MAGNIFICATION_CONTROLLER_NAME); + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + if (getShortcutTargetsInternalLocked(shortcutType) + .contains(MAGNIFICATION_CONTROLLER_NAME)) { + return true; + } + } + return false; } /** diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING new file mode 100644 index 000000000000..91e82ec0e95b --- /dev/null +++ b/services/appfunctions/TEST_MAPPING @@ -0,0 +1,10 @@ +{ + "postsubmit": [ + { + "name": "FrameworksAppFunctionsTests" + }, + { + "name": "CtsAppFunctionTestCases" + } + ] +}
\ No newline at end of file diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java new file mode 100644 index 000000000000..094723814e17 --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2024 The Android Open 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.appfunctions; + +import android.annotation.NonNull; +import android.app.appsearch.AppSearchBatchResult; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchManager.SearchContext; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.AppSearchSession; +import android.app.appsearch.BatchResultCallback; +import android.app.appsearch.GenericDocument; +import android.app.appsearch.GetByDocumentIdRequest; +import android.app.appsearch.GetSchemaResponse; +import android.app.appsearch.PutDocumentsRequest; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchResults; +import android.app.appsearch.SearchSpec; +import android.app.appsearch.SetSchemaRequest; +import android.app.appsearch.SetSchemaResponse; +import android.util.Slog; + +import com.android.internal.infra.AndroidFuture; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** A future API wrapper of {@link AppSearchSession} APIs. */ +public class FutureAppSearchSession implements Closeable { + private static final String TAG = FutureAppSearchSession.class.getSimpleName(); + private final Executor mExecutor; + private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture; + + public FutureAppSearchSession( + @NonNull AppSearchManager appSearchManager, + @NonNull Executor executor, + @NonNull SearchContext appSearchContext) { + Objects.requireNonNull(appSearchManager); + Objects.requireNonNull(executor); + Objects.requireNonNull(appSearchContext); + + mExecutor = executor; + mSettableSessionFuture = new AndroidFuture<>(); + appSearchManager.createSearchSession( + appSearchContext, mExecutor, mSettableSessionFuture::complete); + } + + /** Converts a failed app search result codes into an exception. */ + @NonNull + public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) { + return switch (appSearchResult.getResultCode()) { + case AppSearchResult.RESULT_INVALID_ARGUMENT -> + new IllegalArgumentException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_IO_ERROR -> + new IOException(appSearchResult.getErrorMessage()); + case AppSearchResult.RESULT_SECURITY_ERROR -> + new SecurityException(appSearchResult.getErrorMessage()); + default -> new IllegalStateException(appSearchResult.getErrorMessage()); + }; + } + + private AndroidFuture<AppSearchSession> getSessionAsync() { + return mSettableSessionFuture.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException(failedResultToException(result)); + } + }); + } + + /** Gets the schema for a given app search session. */ + public AndroidFuture<GetSchemaResponse> getSchema() { + return getSessionAsync() + .thenCompose( + session -> { + AndroidFuture<AppSearchResult<GetSchemaResponse>> + settableSchemaResponse = new AndroidFuture<>(); + session.getSchema(mExecutor, settableSchemaResponse::complete); + return settableSchemaResponse.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException( + failedResultToException(result)); + } + }); + }); + } + + /** Sets the schema for a given app search session. */ + public AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest) { + return getSessionAsync() + .thenCompose( + session -> { + AndroidFuture<AppSearchResult<SetSchemaResponse>> + settableSchemaResponse = new AndroidFuture<>(); + session.setSchema( + setSchemaRequest, + mExecutor, + mExecutor, + settableSchemaResponse::complete); + return settableSchemaResponse.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException( + failedResultToException(result)); + } + }); + }); + } + + /** Indexes documents into the AppSearchSession database. */ + public AndroidFuture<AppSearchBatchResult<String, Void>> put( + @NonNull PutDocumentsRequest putDocumentsRequest) { + return getSessionAsync() + .thenCompose( + session -> { + AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture = + new AndroidFuture<>(); + + session.put( + putDocumentsRequest, mExecutor, batchResultFuture::complete); + return batchResultFuture; + }); + } + + /** + * Retrieves documents from the open AppSearchSession that match a given query string and type + * of search provided. + */ + public AndroidFuture<FutureSearchResults> search( + @NonNull String queryExpression, @NonNull SearchSpec searchSpec) { + return getSessionAsync() + .thenApply(session -> session.search(queryExpression, searchSpec)) + .thenApply(result -> new FutureSearchResults(result, mExecutor)); + } + + @Override + public void close() throws IOException { + try { + getSessionAsync().get().close(); + } catch (Exception ex) { + Slog.e(TAG, "Failed to close app search session", ex); + } + } + + /** A future API wrapper of {@link android.app.appsearch.SearchResults}. */ + public static class FutureSearchResults { + private final SearchResults mSearchResults; + private final Executor mExecutor; + + public FutureSearchResults( + @NonNull SearchResults searchResults, @NonNull Executor executor) { + mSearchResults = Objects.requireNonNull(searchResults); + mExecutor = Objects.requireNonNull(executor); + } + + public AndroidFuture<List<SearchResult>> getNextPage() { + AndroidFuture<AppSearchResult<List<SearchResult>>> nextPageFuture = + new AndroidFuture<>(); + + mSearchResults.getNextPage(mExecutor, nextPageFuture::complete); + return nextPageFuture.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException(failedResultToException(result)); + } + }); + } + } + + /** A future API to retrieve a document by its id from the local AppSearch session. */ + public AndroidFuture<GenericDocument> getByDocumentId( + @NonNull String documentId, @NonNull String namespace) { + Objects.requireNonNull(documentId); + Objects.requireNonNull(namespace); + + GetByDocumentIdRequest request = + new GetByDocumentIdRequest.Builder(namespace) + .addIds(documentId) + .build(); + return getSessionAsync() + .thenCompose( + session -> { + AndroidFuture<AppSearchBatchResult<String, GenericDocument>> + batchResultFuture = new AndroidFuture<>(); + session.getByDocumentId( + request, + mExecutor, + new BatchResultCallbackAdapter<>(batchResultFuture)); + + return batchResultFuture.thenApply( + batchResult -> + getGenericDocumentFromBatchResult( + batchResult, documentId)); + }); + } + + private static GenericDocument getGenericDocumentFromBatchResult( + AppSearchBatchResult<String, GenericDocument> result, String documentId) { + if (result.isSuccess()) { + return result.getSuccesses().get(documentId); + } + throw new IllegalArgumentException("No document in the result for id: " + documentId); + } + + private static final class BatchResultCallbackAdapter<K, V> + implements BatchResultCallback<K, V> { + private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture; + + BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) { + mFuture = future; + } + + @Override + public void onResult(@NonNull AppSearchBatchResult<K, V> result) { + mFuture.complete(result); + } + + @Override + public void onSystemError(Throwable t) { + mFuture.completeExceptionally(t); + } + } +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java new file mode 100644 index 000000000000..0c2262456032 --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open 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.appfunctions; + +import android.annotation.NonNull; +import android.app.appsearch.AppSearchManager; +import android.app.appsearch.AppSearchResult; +import android.app.appsearch.GlobalSearchSession; +import android.app.appsearch.exceptions.AppSearchException; +import android.app.appsearch.observer.ObserverCallback; +import android.app.appsearch.observer.ObserverSpec; +import android.util.Slog; + +import com.android.internal.infra.AndroidFuture; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Executor; + +/** A wrapper around {@link GlobalSearchSession} that provides a future-based API. */ +public class FutureGlobalSearchSession implements Closeable { + private static final String TAG = FutureGlobalSearchSession.class.getSimpleName(); + private final Executor mExecutor; + private final AndroidFuture<AppSearchResult<GlobalSearchSession>> mSettableSessionFuture; + + public FutureGlobalSearchSession( + @NonNull AppSearchManager appSearchManager, @NonNull Executor executor) { + this.mExecutor = executor; + mSettableSessionFuture = new AndroidFuture<>(); + appSearchManager.createGlobalSearchSession(mExecutor, mSettableSessionFuture::complete); + } + + private AndroidFuture<GlobalSearchSession> getSessionAsync() { + return mSettableSessionFuture.thenApply( + result -> { + if (result.isSuccess()) { + return result.getResultValue(); + } else { + throw new RuntimeException( + FutureAppSearchSession.failedResultToException(result)); + } + }); + } + + /** + * Registers an observer callback for the given target package name. + * + * @param targetPackageName The package name of the target app. + * @param spec The observer spec. + * @param executor The executor to run the observer callback on. + * @param observer The observer callback to register. + * @return A future that completes once the observer is registered. + */ + public AndroidFuture<Void> registerObserverCallbackAsync( + String targetPackageName, + ObserverSpec spec, + Executor executor, + ObserverCallback observer) { + return getSessionAsync() + .thenCompose( + session -> { + try { + session.registerObserverCallback( + targetPackageName, spec, executor, observer); + return AndroidFuture.completedFuture(null); + } catch (AppSearchException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void close() throws IOException { + try { + getSessionAsync().get().close(); + } catch (Exception ex) { + Slog.e(TAG, "Failed to close global search session", ex); + } + } +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java new file mode 100644 index 000000000000..be5770b280dc --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open 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.appfunctions; + +import android.annotation.NonNull; +import android.annotation.WorkerThread; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchSpec; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +/** + * This class implements helper methods for synchronously interacting with AppSearch while + * synchronizing AppFunction runtime and static metadata. + */ +public class MetadataSyncAdapter { + private final FutureAppSearchSession mFutureAppSearchSession; + private final Executor mSyncExecutor; + + public MetadataSyncAdapter( + @NonNull Executor syncExecutor, + @NonNull FutureAppSearchSession futureAppSearchSession) { + mSyncExecutor = Objects.requireNonNull(syncExecutor); + mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession); + } + + /** + * This method returns a map of package names to a set of function ids that are in the static + * metadata but not in the runtime metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the static metadata but + * not in the runtime metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); + } + + /** + * This method returns a map of package names to a set of function ids that are in the runtime + * metadata but not in the static metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the runtime metadata but + * not in the static metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap); + } + + @NonNull + private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> packageToFunctionMapA, + ArrayMap<String, ArraySet<String>> packageToFunctionMapB) { + ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>(); + for (String packageName : packageToFunctionMapA.keySet()) { + if (!packageToFunctionMapB.containsKey(packageName)) { + diffMap.put(packageName, packageToFunctionMapA.get(packageName)); + continue; + } + ArraySet<String> diffFunctions = new ArraySet<>(); + for (String functionId : + Objects.requireNonNull(packageToFunctionMapA.get(packageName))) { + if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName)) + .contains(functionId)) { + diffFunctions.add(functionId); + } + } + if (!diffFunctions.isEmpty()) { + diffMap.put(packageName, diffFunctions); + } + } + return diffMap; + } + + /** + * This method returns a map of package names to a set of function ids. + * + * @param queryExpression The query expression to use when searching for AppFunction metadata. + * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata. + * @return A map of package names to a set of function ids. + * @throws ExecutionException If the future search results fail to execute. + * @throws InterruptedException If the future search results are interrupted. + */ + @NonNull + @VisibleForTesting + @WorkerThread + ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap( + @NonNull String queryExpression, + @NonNull SearchSpec metadataSearchSpec, + @NonNull String propertyFunctionId, + @NonNull String propertyPackageName) + throws ExecutionException, InterruptedException { + ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>(); + FutureSearchResults futureSearchResults = + mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get(); + List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); + // TODO(b/357551503): This could be expensive if we have more functions + while (!searchResultsList.isEmpty()) { + for (SearchResult searchResult : searchResultsList) { + String packageName = + searchResult.getGenericDocument().getPropertyString(propertyPackageName); + String functionId = + searchResult.getGenericDocument().getPropertyString(propertyFunctionId); + packageToFunctionIds + .computeIfAbsent(packageName, k -> new ArraySet<>()) + .add(functionId); + } + searchResultsList = futureSearchResults.getNextPage().get(); + } + return packageToFunctionIds; + } +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java index 98903ae57a39..58597c38bb94 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java @@ -25,7 +25,6 @@ import android.os.UserHandle; * services are properly unbound after the operation completes or a timeout occurs. * * @param <T> Class of wrapped service. - * @hide */ public interface RemoteServiceCaller<T> { diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java index 0e18705c40b0..eea17eeca371 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java @@ -34,7 +34,6 @@ import java.util.function.Function; * Context#bindService}. * * @param <T> Class of wrapped service. - * @hide */ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { private static final String TAG = "AppFunctionsServiceCall"; diff --git a/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java b/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java deleted file mode 100644 index e56f81f0034a..000000000000 --- a/services/appfunctions/java/com/android/server/appfunctions/SyncAppSearchCallHelper.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open 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.appfunctions; - -import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.app.appsearch.AppSearchManager; -import android.app.appsearch.AppSearchManager.SearchContext; -import android.app.appsearch.AppSearchResult; -import android.app.appsearch.AppSearchSession; -import android.app.appsearch.GetSchemaResponse; -import android.app.appsearch.SetSchemaRequest; -import android.app.appsearch.SetSchemaResponse; -import android.util.Slog; - -import com.android.internal.infra.AndroidFuture; - -import java.io.Closeable; -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.Executor; - -/** - * Helper class for interacting with a system server local appsearch session asynchronously. - * - * <p>Converts the AppSearch Callback API to {@link AndroidFuture}. - */ -@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) -public class SyncAppSearchCallHelper implements Closeable { - private static final String TAG = SyncAppSearchCallHelper.class.getSimpleName(); - private final Executor mExecutor; - private final AppSearchManager mAppSearchManager; - private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture; - - public SyncAppSearchCallHelper( - @NonNull AppSearchManager appSearchManager, - @NonNull Executor executor, - @NonNull SearchContext appSearchContext) { - Objects.requireNonNull(appSearchManager); - Objects.requireNonNull(executor); - Objects.requireNonNull(appSearchContext); - - mExecutor = executor; - mAppSearchManager = appSearchManager; - mSettableSessionFuture = new AndroidFuture<>(); - mAppSearchManager.createSearchSession( - appSearchContext, mExecutor, mSettableSessionFuture::complete); - } - - /** Converts a failed app search result codes into an exception. */ - @NonNull - private static Exception failedResultToException(@NonNull AppSearchResult appSearchResult) { - return switch (appSearchResult.getResultCode()) { - case AppSearchResult.RESULT_INVALID_ARGUMENT -> - new IllegalArgumentException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_IO_ERROR -> - new IOException(appSearchResult.getErrorMessage()); - case AppSearchResult.RESULT_SECURITY_ERROR -> - new SecurityException(appSearchResult.getErrorMessage()); - default -> new IllegalStateException(appSearchResult.getErrorMessage()); - }; - } - - private AndroidFuture<AppSearchSession> getSessionAsync() { - return mSettableSessionFuture.thenApply( - result -> { - if (result.isSuccess()) { - return result.getResultValue(); - } else { - throw new RuntimeException(failedResultToException(result)); - } - }); - } - - /** Gets the schema for a given app search session. */ - public AndroidFuture<GetSchemaResponse> getSchema() { - return getSessionAsync() - .thenComposeAsync( - session -> { - AndroidFuture<AppSearchResult<GetSchemaResponse>> - settableSchemaResponse = new AndroidFuture<>(); - session.getSchema(mExecutor, settableSchemaResponse::complete); - return settableSchemaResponse.thenApply( - result -> { - if (result.isSuccess()) { - return result.getResultValue(); - } else { - throw new RuntimeException( - failedResultToException(result)); - } - }); - }, - mExecutor); - } - - /** Sets the schema for a given app search session. */ - public AndroidFuture<SetSchemaResponse> setSchema(@NonNull SetSchemaRequest setSchemaRequest) { - return getSessionAsync() - .thenComposeAsync( - session -> { - AndroidFuture<AppSearchResult<SetSchemaResponse>> - settableSchemaResponse = new AndroidFuture<>(); - session.setSchema( - setSchemaRequest, - mExecutor, - mExecutor, - settableSchemaResponse::complete); - return settableSchemaResponse.thenApply( - result -> { - if (result.isSuccess()) { - return result.getResultValue(); - } else { - throw new RuntimeException( - failedResultToException(result)); - } - }); - }, - mExecutor); - } - - @Override - public void close() throws IOException { - try { - getSessionAsync().get().close(); - } catch (Exception ex) { - Slog.e(TAG, "Failed to close app search session", ex); - } - } -} diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 30683985f047..b53bf984880d 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -220,6 +220,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku // See {@link Provider#pendingDeletedWidgetIds}. private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids"; + // Hard limit of number of hosts an app can create, note that the app that hosts the widgets + // can have multiple instances of {@link AppWidgetHost}, typically in respect to different + // surfaces in the host app. + // @see AppWidgetHost + // @see AppWidgetHost#mHostId + private static final int MAX_NUMBER_OF_HOSTS_PER_PACKAGE = 20; + // Hard limit of number of widgets can be pinned by a host. + private static final int MAX_NUMBER_OF_WIDGETS_PER_HOST = 200; + // Handles user and package related broadcasts. // See {@link #registerBroadcastReceiver} private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @@ -2284,7 +2293,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (host != null) { return host; } - + ensureHostCountBeforeAddLocked(id); host = new Host(); host.id = id; mHosts.add(host); @@ -2292,6 +2301,24 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku return host; } + /** + * Ensures that the number of hosts for a package is less than the maximum number of hosts per + * package. If the number of hosts is greater than the maximum number of hosts per package, then + * removes the oldest host. + */ + private void ensureHostCountBeforeAddLocked(@NonNull final HostId hostId) { + final List<Host> hosts = new ArrayList<>(); + for (Host host : mHosts) { + if (host.id.uid == hostId.uid + && host.id.packageName.equals(hostId.packageName)) { + hosts.add(host); + } + } + while (hosts.size() >= MAX_NUMBER_OF_HOSTS_PER_PACKAGE) { + deleteHostLocked(hosts.remove(0)); + } + } + private void deleteHostLocked(Host host) { if (DEBUG) { Slog.i(TAG, "deleteHostLocked() " + host); @@ -2377,6 +2404,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } @Override + public void onNullBinding(ComponentName name) { + mContext.unbindService(this); + } + + @Override public void onServiceDisconnected(ComponentName name) { // Do nothing } @@ -2524,6 +2556,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } @Override + public void onNullBinding(ComponentName name) { + mContext.unbindService(this); + } + + @Override public void onServiceDisconnected(android.content.ComponentName name) { // Do nothing } @@ -3582,12 +3619,33 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (DEBUG) { Slog.i(TAG, "addWidgetLocked() " + widget); } + ensureWidgetCountBeforeAddLocked(widget); mWidgets.add(widget); onWidgetProviderAddedOrChangedLocked(widget); } /** + * Ensures that the widget count for the widget's host is not greater than the maximum + * number of widgets per host. If the count is greater than the maximum, removes oldest widgets + * from the host until the count is less than or equal to the maximum. + */ + private void ensureWidgetCountBeforeAddLocked(@NonNull final Widget widget) { + if (widget.host == null || widget.host.id == null) { + return; + } + final List<Widget> widgetsInSameHost = new ArrayList<>(); + for (Widget w : mWidgets) { + if (w.host != null && widget.host.id.equals(w.host.id)) { + widgetsInSameHost.add(w); + } + } + while (widgetsInSameHost.size() >= MAX_NUMBER_OF_WIDGETS_PER_HOST) { + removeWidgetLocked(widgetsInSameHost.remove(0)); + } + } + + /** * Checks if the provider is assigned and updates the mWidgetPackages to track packages * that have bound widgets. */ diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index d4f729cfbaf6..36665240c16b 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -1516,9 +1516,8 @@ public final class ActiveServices { serviceName, FrameworkStatsLog.SERVICE_STATE_CHANGED__STATE__START); mAm.mBatteryStatsService.noteServiceStartRunning(uid, packageName, serviceName); final ProcessRecord hostApp = r.app; - final boolean wasStopped = hostApp == null ? wasStopped(r) : false; - final boolean firstLaunch = - hostApp == null ? !mAm.wasPackageEverLaunched(r.packageName, r.userId) : false; + final boolean wasStopped = hostApp == null ? r.appInfo.isStopped() : false; + final boolean firstLaunch = hostApp == null ? r.appInfo.isNotLaunched() : false; String error = bringUpServiceLocked(r, service.getFlags(), callerFg, false /* whileRestarting */, @@ -4308,9 +4307,8 @@ public final class ActiveServices { true, UNKNOWN_ADJ); } - final boolean wasStopped = hostApp == null ? wasStopped(s) : false; - final boolean firstLaunch = - hostApp == null ? !mAm.wasPackageEverLaunched(s.packageName, s.userId) : false; + final boolean wasStopped = hostApp == null ? s.appInfo.isStopped() : false; + final boolean firstLaunch = hostApp == null ? s.appInfo.isNotLaunched() : false; boolean needOomAdj = false; if (c.hasFlag(Context.BIND_AUTO_CREATE)) { @@ -9350,8 +9348,4 @@ public final class ActiveServices { return mCachedDeviceProvisioningPackage != null && mCachedDeviceProvisioningPackage.equals(packageName); } - - private boolean wasStopped(ServiceRecord serviceRecord) { - return (serviceRecord.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0; - } } diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 1b00cec90bc4..6aadcdc74870 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -33,6 +33,7 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.icu.text.SimpleDateFormat; import android.os.Binder; +import android.os.Debug; import android.os.FileUtils; import android.os.Handler; import android.os.IBinder.DeathRecipient; @@ -495,6 +496,10 @@ public final class AppStartInfoTracker { private void addBaseFieldsFromProcessRecord(ApplicationStartInfo start, ProcessRecord app) { if (app == null) { + if (DEBUG) { + Slog.w(TAG, + "app is null in addBaseFieldsFromProcessRecord: " + Debug.getCallers(4)); + } return; } final int definingUid = app.getHostingRecord() != null diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 955b75d8bba0..3f4902db70f5 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -1122,7 +1122,11 @@ public final class BatteryStatsService extends IBatteryStats.Stub throw new UnsupportedOperationException("Unknown tagId=" + atomTag); } final byte[] statsProto = bus.getStatsProto(); - + try { + bus.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure close BatteryUsageStats", e); + } data.add(FrameworkStatsLog.buildStatsEvent(atomTag, statsProto)); return StatsManager.PULL_SUCCESS; diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 8fe33d18152c..9e4666cca140 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -1005,13 +1005,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final ApplicationInfo info = ((ResolveInfo) receiver).activityInfo.applicationInfo; final ComponentName component = ((ResolveInfo) receiver).activityInfo.getComponentName(); - if ((info.flags & ApplicationInfo.FLAG_STOPPED) != 0) { - queue.setActiveWasStopped(true); - } - final int intentFlags = r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND; - final boolean firstLaunch = !mService.wasPackageEverLaunched(info.packageName, r.userId); - queue.setActiveFirstLaunch(firstLaunch); + queue.setActiveWasStopped(info.isStopped()); + queue.setActiveFirstLaunch(info.isNotLaunched()); + final int intentFlags = r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND; final HostingRecord hostingRecord = new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, component, r.intent.getAction(), r.getHostingRecordTriggerType()); final boolean isActivityCapable = (r.options != null diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 8f52f67ff7e0..416c11090515 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -86,7 +86,6 @@ import com.android.server.ServiceThread; import dalvik.annotation.optimization.NeverCompile; -import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -2318,6 +2317,7 @@ public final class CachedAppOptimizer { Slog.d(TAG_AM, "Skipping freeze because process is marked " + "should not be frozen"); } + reportProcessFreezableChangedLocked(proc); return; } diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 13145214c1c1..da408266bfac 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -554,13 +554,11 @@ public class ContentProviderHelper { callingProcessState, proc.mState.getCurProcState(), false, 0L); } else { - final boolean stopped = - (cpr.appInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0; + final boolean stopped = cpr.appInfo.isStopped(); final int packageState = stopped ? PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_STOPPED : PROVIDER_ACQUISITION_EVENT_REPORTED__PACKAGE_STOPPED_STATE__PACKAGE_STATE_NORMAL; - final boolean firstLaunch = !mService.wasPackageEverLaunched( - cpi.packageName, userId); + final boolean firstLaunch = cpr.appInfo.isNotLaunched(); checkTime(startTime, "getContentProviderImpl: before start process"); proc = mService.startProcessLocked( cpi.processName, cpr.appInfo, false, 0, diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index bb0c24b4f8c6..5b4e57350c40 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -3394,24 +3394,33 @@ public final class ProcessList { hostingRecord.getDefiningUid(), hostingRecord.getDefiningProcessName()); final ProcessStateRecord state = r.mState; - final boolean wasStopped = (info.flags & ApplicationInfo.FLAG_STOPPED) != 0; + final boolean wasStopped = info.isStopped(); // Check if we should mark the processrecord for first launch after force-stopping if (wasStopped) { + boolean wasEverLaunched; + if (android.app.Flags.useAppInfoNotLaunched()) { + wasEverLaunched = !info.isNotLaunched(); + } else { + wasEverLaunched = mService.getPackageManagerInternal() + .wasPackageEverLaunched(r.getApplicationInfo().packageName, r.userId); + } // Check if the hosting record is for an activity or not. Since the stopped // state tracking is handled differently to avoid WM calling back into AM, // store the state in the correct record if (hostingRecord.isTypeActivity()) { - final boolean wasPackageEverLaunched = mService - .wasPackageEverLaunched(r.getApplicationInfo().packageName, r.userId); // If the package was launched in the past but is currently stopped, only then // should it be considered as force-stopped. - @WindowProcessController.StoppedState int stoppedState = wasPackageEverLaunched + @WindowProcessController.StoppedState int stoppedState = wasEverLaunched ? STOPPED_STATE_FORCE_STOPPED : STOPPED_STATE_FIRST_LAUNCH; r.getWindowProcessController().setStoppedState(stoppedState); } else { - r.setWasForceStopped(true); - // first launch is computed just before logging, for non-activity types + if (android.app.Flags.useAppInfoNotLaunched()) { + // If it was launched before, then it must be a force-stop + r.setWasForceStopped(wasEverLaunched); + } else { + r.setWasForceStopped(true); + } } } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 29373076c3b8..99c3ecaba2e0 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -139,6 +139,7 @@ public class SettingsToPropertiesMapper { static final String[] sDeviceConfigAconfigScopes = new String[] { "accessibility", "android_core_networking", + "android_health_services", "android_sdk", "android_stylus", "aoc", @@ -235,7 +236,6 @@ public class SettingsToPropertiesMapper { "wear_connectivity", "wear_esim_carriers", "wear_frameworks", - "wear_health_services", "wear_media", "wear_offload", "wear_security", diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 780eda604436..6daf0d0b7d3b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -12712,11 +12712,6 @@ public class AudioService extends IAudioService.Stub if (mController == null) return; try { - // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO - if (isStreamBluetoothSco(streamType)) { - // TODO: notify both sco and voice_call about volume changes - streamType = AudioSystem.STREAM_BLUETOOTH_SCO; - } mController.volumeChanged(streamType, flags); } catch (RemoteException e) { Log.w(TAG, "Error calling volumeChanged", e); @@ -14727,6 +14722,7 @@ public class AudioService extends IAudioService.Stub @Override /** @see AudioManager#permissionUpdateBarrier() */ public void permissionUpdateBarrier() { + if (!audioserverPermissions()) return; mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle); List<Future> snapshot; synchronized (mScheduledPermissionTasks) { diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index ac3c02823d0a..b2c616ae5b3c 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -316,6 +316,7 @@ class PreAuthInfo { Pair<BiometricSensor, Integer> sensorNotEnrolled = null; Pair<BiometricSensor, Integer> sensorLockout = null; Pair<BiometricSensor, Integer> hardwareNotDetected = null; + Pair<BiometricSensor, Integer> biometricAppNotAllowed = null; for (Pair<BiometricSensor, Integer> pair : ineligibleSensors) { final int status = pair.second; if (status == BIOMETRIC_LOCKOUT_TIMED || status == BIOMETRIC_LOCKOUT_PERMANENT) { @@ -327,6 +328,9 @@ class PreAuthInfo { if (status == BIOMETRIC_HARDWARE_NOT_DETECTED) { hardwareNotDetected = pair; } + if (status == BIOMETRIC_NOT_ENABLED_FOR_APPS) { + biometricAppNotAllowed = pair; + } } // If there is a sensor locked out, prioritize lockout over other sensor's error. @@ -339,6 +343,10 @@ class PreAuthInfo { return hardwareNotDetected; } + if (Flags.mandatoryBiometrics() && biometricAppNotAllowed != null) { + return biometricAppNotAllowed; + } + // If the caller requested STRONG, and the device contains both STRONG and non-STRONG // sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's // BIOMETRIC_INSUFFICIENT_STRENGTH error. diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 871121472938..407ef1e41aa6 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -321,6 +321,9 @@ public class Utils { case BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE: biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; break; + case BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS: + biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + break; default: Slog.e(BiometricService.TAG, "Unhandled result code: " + biometricConstantsCode); biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE; @@ -384,9 +387,12 @@ public class Utils { return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED; case MANDATORY_BIOMETRIC_UNAVAILABLE_ERROR: return BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; + case BIOMETRIC_NOT_ENABLED_FOR_APPS: + if (Flags.mandatoryBiometrics()) { + return BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + } case BIOMETRIC_DISABLED_BY_DEVICE_POLICY: case BIOMETRIC_HARDWARE_NOT_DETECTED: - case BIOMETRIC_NOT_ENABLED_FOR_APPS: default: return BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE; } diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 17835b2d085b..ec61d4d39aa0 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -379,9 +379,7 @@ public class CameraServiceProxy extends SystemService streamCount = mStreamStats.size(); } if (CameraServiceProxy.DEBUG) { - String ultrawideDebug = Flags.logUltrawideUsage() - ? ", wideAngleUsage " + mUsedUltraWide - : ""; + String ultrawideDebug = ", wideAngleUsage " + mUsedUltraWide; String zoomOverrideDebug = Flags.logZoomOverrideUsage() ? ", zoomOverrideUsage " + mUsedZoomOverride : ""; @@ -1338,7 +1336,7 @@ public class CameraServiceProxy extends SystemService List<CameraStreamStats> streamStats = cameraState.getStreamStats(); String userTag = cameraState.getUserTag(); int videoStabilizationMode = cameraState.getVideoStabilizationMode(); - boolean usedUltraWide = Flags.logUltrawideUsage() ? cameraState.getUsedUltraWide() : false; + boolean usedUltraWide = cameraState.getUsedUltraWide(); boolean usedZoomOverride = Flags.logZoomOverrideUsage() ? cameraState.getUsedZoomOverride() : false; long logId = cameraState.getLogId(); diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 907e7c639352..86015acc232f 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -938,7 +938,7 @@ public class AutomaticBrightnessController { setAmbientLux(mFastAmbientLux); if (mLoggingEnabled) { Slog.d(TAG, "updateAmbientLux: " - + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": " + + ((mFastAmbientLux > mPreThresholdLux) ? "Brightened" : "Darkened") + ": " + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", " + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", " + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index 334dda079e7a..01bbd2fb39f8 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -17,6 +17,7 @@ package com.android.server.display; import android.hardware.display.BrightnessInfo; +import android.os.PowerManager; import android.text.TextUtils; import com.android.server.display.brightness.BrightnessEvent; @@ -255,7 +256,7 @@ public final class DisplayBrightnessState { private String mDisplayBrightnessStrategyName; private boolean mShouldUseAutoBrightness; private boolean mIsSlowChange; - private float mMaxBrightness; + private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX; private float mMinBrightness; private float mCustomAnimationRate = CUSTOM_ANIMATION_RATE_NOT_SET; private boolean mShouldUpdateScreenBrightnessSetting; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 2b732eab67cc..ed16b1472ee5 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -134,6 +134,7 @@ import android.util.ArraySet; import android.util.EventLog; import android.util.IndentingPrintWriter; import android.util.IntArray; +import android.util.MathUtils; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; @@ -1275,6 +1276,9 @@ public final class DisplayManagerService extends SystemService { || isUidPresentOnDisplayInternal(callingUid, displayId)) { return info; } + } else if (displayId == Display.DEFAULT_DISPLAY) { + Slog.e(TAG, "Default display is null for info request from uid " + + callingUid); } return null; } @@ -2223,10 +2227,11 @@ public final class DisplayManagerService extends SystemService { if (display.isValidLocked()) { applyDisplayChangedLocked(display); } - return; + } else { + releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); } - releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + Slog.i(TAG, "Logical display removed: " + display.getDisplayIdLocked()); } private void releaseDisplayAndEmitEvent(LogicalDisplay display, int event) { @@ -3098,6 +3103,7 @@ public final class DisplayManagerService extends SystemService { /** * Get internal or external viewport. Create it if does not currently exist. + * * @param viewportType - either INTERNAL or EXTERNAL * @return the viewport with the requested type */ @@ -4413,7 +4419,6 @@ public final class DisplayManagerService extends SystemService { } - @Override // Binder call public BrightnessConfiguration getBrightnessConfigurationForUser(int userId) { final String uniqueId; @@ -4492,10 +4497,12 @@ public final class DisplayManagerService extends SystemService { @Override // Binder call public void setBrightness(int displayId, float brightness) { setBrightness_enforcePermission(); - if (!isValidBrightness(brightness)) { - Slog.w(TAG, "Attempted to set invalid brightness" + brightness); + if (Float.isNaN(brightness)) { + Slog.w(TAG, "Attempted to set invalid brightness: " + brightness); return; } + MathUtils.constrain(brightness, PowerManager.BRIGHTNESS_MIN, + PowerManager.BRIGHTNESS_MAX); final long token = Binder.clearCallingIdentity(); try { synchronized (mSyncRoot) { @@ -4791,12 +4798,6 @@ public final class DisplayManagerService extends SystemService { } } - private static boolean isValidBrightness(float brightness) { - return !Float.isNaN(brightness) - && (brightness >= PowerManager.BRIGHTNESS_MIN) - && (brightness <= PowerManager.BRIGHTNESS_MAX); - } - @VisibleForTesting void overrideSensorManager(SensorManager sensorManager) { synchronized (mSyncRoot) { diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index bb2bed7281f7..7c591e3a2c03 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -2222,6 +2222,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call unblockScreenOn(); } mWindowManagerPolicy.screenTurningOn(mDisplayId, mPendingScreenOnUnblocker); + Slog.i(TAG, "Window Manager Policy screenTurningOn complete"); } // Return true if the screen isn't blocked. diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java index 40e9198ea921..cf44ac029c82 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java @@ -73,7 +73,6 @@ abstract class BrightnessClamper<T> { abstract void stop(); protected enum Type { - THERMAL, POWER, WEAR_BEDTIME_MODE, } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index d3be33f51e5c..9404034cdd34 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -72,6 +72,8 @@ public class BrightnessClamperController { private final List<DisplayDeviceDataListener> mDisplayDeviceDataListeners = new ArrayList<>(); private final List<StatefulModifier> mStatefulModifiers = new ArrayList<>(); private final List<UserSwitchListener> mUserSwitchListeners = new ArrayList<>(); + private final List<DeviceConfigListener> mDeviceConfigListeners = new ArrayList<>(); + private ModifiersAggregatedState mModifiersAggregatedState = new ModifiersAggregatedState(); private final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; @@ -144,9 +146,14 @@ public class BrightnessClamperController { if (m instanceof UserSwitchListener l) { mUserSwitchListeners.add(l); } + if (m instanceof DeviceConfigListener l) { + mDeviceConfigListeners.add(l); + } }); - mOnPropertiesChangedListener = - properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); + mOnPropertiesChangedListener = properties -> { + mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); + mDeviceConfigListeners.forEach(DeviceConfigListener::onDeviceConfigChanged); + }; mLightSensorController.configure(data.getAmbientLightSensor(), data.getDisplayId()); start(); } @@ -209,8 +216,6 @@ public class BrightnessClamperController { private int getBrightnessMaxReason() { if (mClamperType == null) { return BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; - } else if (mClamperType == Type.THERMAL) { - return BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL; } else if (mClamperType == Type.POWER) { return BrightnessInfo.BRIGHTNESS_MAX_REASON_POWER_IC; } else if (mClamperType == Type.WEAR_BEDTIME_MODE) { @@ -225,7 +230,7 @@ public class BrightnessClamperController { * Called when the user switches. */ public void onUserSwitch() { - mUserSwitchListeners.forEach(listener -> listener.onSwitchUser()); + mUserSwitchListeners.forEach(UserSwitchListener::onSwitchUser); } /** @@ -294,11 +299,14 @@ public class BrightnessClamperController { state2.mMaxDesiredHdrRatio) || !BrightnessSynchronizer.floatEquals(state1.mMaxHdrBrightness, state2.mMaxHdrBrightness) - || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline; + || state1.mSdrHdrRatioSpline != state2.mSdrHdrRatioSpline + || state1.mMaxBrightnessReason != state2.mMaxBrightnessReason + || !BrightnessSynchronizer.floatEquals(state1.mMaxBrightness, + state2.mMaxBrightness); } private void start() { - if (!mClampers.isEmpty()) { + if (!mClampers.isEmpty() || !mDeviceConfigListeners.isEmpty()) { mDeviceConfigParameterProvider.addOnPropertiesChangedListener( mExecutor, mOnPropertiesChangedListener); } @@ -333,8 +341,7 @@ public class BrightnessClamperController { ClamperChangeListener clamperChangeListener, DisplayDeviceData data, DisplayManagerFlags flags, Context context, float currentBrightness) { List<BrightnessClamper<? super DisplayDeviceData>> clampers = new ArrayList<>(); - clampers.add( - new BrightnessThermalClamper(handler, clamperChangeListener, data)); + if (flags.isPowerThrottlingClamperEnabled()) { // Check if power-throttling config is present. PowerThrottlingConfigData configData = data.getPowerThrottlingConfigData(); @@ -354,6 +361,8 @@ public class BrightnessClamperController { Handler handler, ClamperChangeListener listener, DisplayDeviceData data) { List<BrightnessStateModifier> modifiers = new ArrayList<>(); + modifiers.add(new BrightnessThermalModifier(handler, listener, data)); + modifiers.add(new DisplayDimModifier(context)); modifiers.add(new BrightnessLowPowerModeModifier()); if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) { @@ -384,7 +393,7 @@ public class BrightnessClamperController { /** * Config Data for clampers/modifiers */ - public static class DisplayDeviceData implements BrightnessThermalClamper.ThermalData, + public static class DisplayDeviceData implements BrightnessThermalModifier.ThermalData, BrightnessPowerClamper.PowerData, BrightnessWearBedtimeModeClamper.WearBedtimeModeData { @NonNull @@ -498,13 +507,23 @@ public class BrightnessClamperController { } /** + * Modifier should implement this interface in order to receive device config updates + */ + interface DeviceConfigListener { + void onDeviceConfigChanged(); + } + + /** * StatefulModifiers contribute to AggregatedState, that is used to decide if brightness - * adjustement is needed + * adjustment is needed */ public static class ModifiersAggregatedState { float mMaxDesiredHdrRatio = HdrBrightnessModifier.DEFAULT_MAX_HDR_SDR_RATIO; float mMaxHdrBrightness = PowerManager.BRIGHTNESS_MAX; @Nullable Spline mSdrHdrRatioSpline = null; + @BrightnessInfo.BrightnessMaxReason + int mMaxBrightnessReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; + float mMaxBrightness = PowerManager.BRIGHTNESS_MAX; } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalModifier.java index 449825831182..21ef309fb327 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalClamper.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessThermalModifier.java @@ -22,6 +22,8 @@ import static com.android.server.display.brightness.clamper.BrightnessClamperCon import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.display.BrightnessInfo; +import android.hardware.display.DisplayManagerInternal; import android.os.Handler; import android.os.IThermalEventListener; import android.os.IThermalService; @@ -33,8 +35,10 @@ import android.provider.DeviceConfigInterface; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.utils.DeviceConfigParsingUtils; @@ -43,12 +47,15 @@ import com.android.server.display.utils.SensorUtils; import java.io.PrintWriter; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; -class BrightnessThermalClamper extends - BrightnessClamper<BrightnessThermalClamper.ThermalData> { +class BrightnessThermalModifier implements BrightnessStateModifier, + BrightnessClamperController.DisplayDeviceDataListener, + BrightnessClamperController.StatefulModifier, + BrightnessClamperController.DeviceConfigListener { private static final String TAG = "BrightnessThermalClamper"; @NonNull @@ -58,6 +65,11 @@ class BrightnessThermalClamper extends // data from DeviceConfig, for all displays, for all dataSets // mapOf(uniqueDisplayId to mapOf(dataSetId to ThermalBrightnessThrottlingData)) @NonNull + protected final Handler mHandler; + @NonNull + protected final BrightnessClamperController.ClamperChangeListener mChangeListener; + + @NonNull private Map<String, Map<String, ThermalBrightnessThrottlingData>> mThermalThrottlingDataOverride = Map.of(); // data from DisplayDeviceConfig, for particular display+dataSet @@ -73,6 +85,8 @@ class BrightnessThermalClamper extends private String mDataId = null; @Temperature.ThrottlingStatus private int mThrottlingStatus = Temperature.THROTTLING_NONE; + private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX; + private boolean mApplied = false; private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> { try { @@ -88,63 +102,104 @@ class BrightnessThermalClamper extends mDataSetMapper = ThermalBrightnessThrottlingData::create; - BrightnessThermalClamper(Handler handler, ClamperChangeListener listener, - ThermalData thermalData) { - this(new Injector(), handler, listener, thermalData); + BrightnessThermalModifier(Handler handler, ClamperChangeListener listener, + BrightnessClamperController.DisplayDeviceData data) { + this(new Injector(), handler, listener, data); } @VisibleForTesting - BrightnessThermalClamper(Injector injector, Handler handler, - ClamperChangeListener listener, ThermalData thermalData) { - super(handler, listener); + BrightnessThermalModifier(Injector injector, @NonNull Handler handler, + @NonNull ClamperChangeListener listener, + @NonNull BrightnessClamperController.DisplayDeviceData data) { + mHandler = handler; + mChangeListener = listener; mConfigParameterProvider = injector.getDeviceConfigParameterProvider(); mThermalStatusObserver = new ThermalStatusObserver(injector, handler); mHandler.post(() -> { - setDisplayData(thermalData); + setDisplayData(data); loadOverrideData(); }); + } + //region BrightnessStateModifier + @Override + public void apply(DisplayManagerInternal.DisplayPowerRequest request, + DisplayBrightnessState.Builder stateBuilder) { + if (stateBuilder.getMaxBrightness() > mBrightnessCap) { + stateBuilder.setMaxBrightness(mBrightnessCap); + stateBuilder.setBrightness(Math.min(stateBuilder.getBrightness(), mBrightnessCap)); + stateBuilder.setBrightnessMaxReason(BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL); + stateBuilder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_THROTTLED); + // set fast change only when modifier is activated. + // this will allow auto brightness to apply slow change even when modifier is active + if (!mApplied) { + stateBuilder.setIsSlowChange(false); + } + mApplied = true; + } else { + mApplied = false; + } + } + @Override + public void stop() { + mThermalStatusObserver.stopObserving(); } @Override - @NonNull - Type getType() { - return Type.THERMAL; + public void dump(PrintWriter writer) { + writer.println("BrightnessThermalClamper:"); + writer.println(" mThrottlingStatus: " + mThrottlingStatus); + writer.println(" mUniqueDisplayId: " + mUniqueDisplayId); + writer.println(" mDataId: " + mDataId); + writer.println(" mDataOverride: " + mThermalThrottlingDataOverride); + writer.println(" mDataFromDeviceConfig: " + mThermalThrottlingDataFromDeviceConfig); + writer.println(" mDataActive: " + mThermalThrottlingDataActive); + writer.println(" mBrightnessCap:" + mBrightnessCap); + writer.println(" mApplied:" + mApplied); + mThermalStatusObserver.dump(writer); } @Override - void onDeviceConfigChanged() { - mHandler.post(() -> { - loadOverrideData(); - recalculateActiveData(); - }); + public boolean shouldListenToLightSensor() { + return false; + } + + @Override + public void setAmbientLux(float lux) { + // noop } + //endregion + //region DisplayDeviceDataListener @Override - void onDisplayChanged(ThermalData data) { + public void onDisplayChanged(BrightnessClamperController.DisplayDeviceData data) { mHandler.post(() -> { setDisplayData(data); recalculateActiveData(); }); } + //endregion + //region StatefulModifier @Override - void stop() { - mThermalStatusObserver.stopObserving(); + public void applyStateChange( + BrightnessClamperController.ModifiersAggregatedState aggregatedState) { + if (aggregatedState.mMaxBrightness > mBrightnessCap) { + aggregatedState.mMaxBrightness = mBrightnessCap; + aggregatedState.mMaxBrightnessReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL; + } } + //endregion + //region DeviceConfigListener @Override - void dump(PrintWriter writer) { - writer.println("BrightnessThermalClamper:"); - writer.println(" mThrottlingStatus: " + mThrottlingStatus); - writer.println(" mUniqueDisplayId: " + mUniqueDisplayId); - writer.println(" mDataId: " + mDataId); - writer.println(" mDataOverride: " + mThermalThrottlingDataOverride); - writer.println(" mDataFromDeviceConfig: " + mThermalThrottlingDataFromDeviceConfig); - writer.println(" mDataActive: " + mThermalThrottlingDataActive); - mThermalStatusObserver.dump(writer); - super.dump(writer); + public void onDeviceConfigChanged() { + mHandler.post(() -> { + loadOverrideData(); + recalculateActiveData(); + }); } + //endregion private void recalculateActiveData() { if (mUniqueDisplayId == null || mDataId == null) { @@ -176,14 +231,11 @@ class BrightnessThermalClamper extends private void recalculateBrightnessCap() { float brightnessCap = PowerManager.BRIGHTNESS_MAX; - boolean isActive = false; - if (mThermalThrottlingDataActive != null) { // Throttling levels are sorted by increasing severity for (ThrottlingLevel level : mThermalThrottlingDataActive.throttlingLevels) { if (level.thermalStatus <= mThrottlingStatus) { brightnessCap = level.brightness; - isActive = true; } else { // Throttling levels that are greater than the current status are irrelevant break; @@ -191,9 +243,8 @@ class BrightnessThermalClamper extends } } - if (brightnessCap != mBrightnessCap || mIsActive != isActive) { + if (brightnessCap != mBrightnessCap) { mBrightnessCap = brightnessCap; - mIsActive = isActive; mChangeListener.onChanged(); } } @@ -205,7 +256,6 @@ class BrightnessThermalClamper extends } } - private final class ThermalStatusObserver extends IThermalEventListener.Stub { private final Injector mInjector; private final Handler mHandler; @@ -228,7 +278,7 @@ class BrightnessThermalClamper extends String curType = mObserverTempSensor.type; mObserverTempSensor = tempSensor; - if (curType.equals(tempSensor.type)) { + if (Objects.equals(curType, tempSensor.type)) { Slog.d(TAG, "Thermal status observer already started"); return; } diff --git a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java index 234d6d323838..236333ee433d 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecFeatureAction.java @@ -53,6 +53,10 @@ abstract class HdmiCecFeatureAction { // Default state used in common by all the feature actions. protected static final int STATE_NONE = 0; + // Delay to query avr's audio status, some avrs could report the old volume first. It could + // show inverse mute state on TV. + protected static final long DELAY_GIVE_AUDIO_STATUS = 500; + // Internal state indicating the progress of action. protected int mState = STATE_NONE; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 4b85217c2136..101596d9d7c1 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -1150,6 +1150,13 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { return Constants.ABORT_REFUSED; } + if (mArcEstablished) { + HdmiLogger.debug("ARC is already established."); + HdmiCecMessage command = HdmiCecMessageBuilder.buildReportArcInitiated( + getDeviceInfo().getLogicalAddress(), message.getSource()); + mService.sendCecCommand(command); + return Constants.HANDLED; + } // In case where <Initiate Arc> is started by <Request ARC Initiation>, this message is // handled in RequestArcInitiationAction as well. SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this, diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 3c7b9d37e4c6..271836ac7d29 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -1638,6 +1638,10 @@ public class HdmiControlService extends SystemService { mHandler.post(new WorkSourceUidPreservingRunnable(runnable)); } + void runOnServiceThreadDelayed(Runnable runnable, long delay) { + mHandler.postDelayed(new WorkSourceUidPreservingRunnable(runnable), delay); + } + private void assertRunOnServiceThread() { if (Looper.myLooper() != mHandler.getLooper()) { throw new IllegalStateException("Should run on service thread."); diff --git a/services/core/java/com/android/server/hdmi/RequestSadAction.java b/services/core/java/com/android/server/hdmi/RequestSadAction.java index 0188e963140e..25663006b4b1 100644 --- a/services/core/java/com/android/server/hdmi/RequestSadAction.java +++ b/services/core/java/com/android/server/hdmi/RequestSadAction.java @@ -19,6 +19,8 @@ package com.android.server.hdmi; import android.hardware.hdmi.HdmiControlManager; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -36,7 +38,8 @@ final class RequestSadAction extends HdmiCecFeatureAction { // State in which the action is waiting for <Report Short Audio Descriptor>. private static final int STATE_WAITING_FOR_REPORT_SAD = 1; private static final int MAX_SAD_PER_REQUEST = 4; - private static final int RETRY_COUNTER_MAX = 1; + @VisibleForTesting + public static final int RETRY_COUNTER_MAX = 3; private final int mTargetAddress; private final RequestSadCallback mCallback; private final List<Integer> mCecCodecsToQuery = new ArrayList<>(); diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java index 7e18d8412aae..11682f664a15 100644 --- a/services/core/java/com/android/server/hdmi/SendKeyAction.java +++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java @@ -180,15 +180,22 @@ final class SendKeyAction extends HdmiCecFeatureAction { && localDevice().getService().isAbsoluteVolumeBehaviorEnabled()) { sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(), mTargetAddress), - __ -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus( - getSourceAddress(), - localDevice().findAudioReceiverAddress()))); + __ -> queryAvrAudioStatus()); } else { sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(), mTargetAddress)); } } + private void queryAvrAudioStatus() { + localDevice().mService.runOnServiceThreadDelayed( + () -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus( + getSourceAddress(), + localDevice().findAudioReceiverAddress())), + DELAY_GIVE_AUDIO_STATUS); + + } + @Override public boolean processCommand(HdmiCecMessage cmd) { // Send key action doesn't need any incoming CEC command, hence does not consume it. diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java index 5ab22e1dcd61..e6abcb958b55 100644 --- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java +++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java @@ -60,6 +60,12 @@ final class SetArcTransmissionStateAction extends HdmiCecFeatureAction { boolean start() { // Seq #37. if (mEnabled) { + // Avoid triggering duplicate RequestSadAction events. + // This could lead to unexpected responses from the AVR and cause the TV to receive data + // out of order. The SAD report does not provide information about the order of events. + if ((tv().hasAction(RequestSadAction.class))) { + return true; + } // Request SADs before enabling ARC RequestSadAction action = new RequestSadAction( localDevice(), Constants.ADDR_AUDIO_SYSTEM, diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 84cee7ecbd05..1285a61d08f2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2269,13 +2269,15 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) { - // TODO(b/286551975): sent the touchpad hardware state data here to TouchpadDebugActivity Slog.d(TAG, "notifyTouchpadHardwareState: Time: " + hardwareStates.getTimestamp() + ", No. Buttons: " + hardwareStates.getButtonsDown() + ", No. Fingers: " + hardwareStates.getFingerCount() + ", No. Touch: " + hardwareStates.getTouchCount() + ", Id: " + deviceId); + if (mTouchpadDebugViewController != null) { + mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates); + } } // Native callback. diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java index 7785ffb4b17a..ba56ad073e6a 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java @@ -30,6 +30,9 @@ import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; +import com.android.server.input.TouchpadFingerState; +import com.android.server.input.TouchpadHardwareState; + import java.util.Objects; public class TouchpadDebugView extends LinearLayout { @@ -52,6 +55,10 @@ public class TouchpadDebugView extends LinearLayout { private int mScreenHeight; private int mWindowLocationBeforeDragX; private int mWindowLocationBeforeDragY; + @NonNull + private TouchpadHardwareState mLastTouchpadState = + new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0]); public TouchpadDebugView(Context context, int touchpadId) { super(context); @@ -83,14 +90,14 @@ public class TouchpadDebugView extends LinearLayout { private void init(Context context) { setOrientation(VERTICAL); - setLayoutParams(new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - setBackgroundColor(Color.TRANSPARENT); + setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT)); + setBackgroundColor(Color.RED); // TODO(b/286551975): Replace this content with the touchpad debug view. TextView textView1 = new TextView(context); - textView1.setBackgroundColor(Color.parseColor("#FFFF0000")); + textView1.setBackgroundColor(Color.TRANSPARENT); textView1.setTextSize(20); textView1.setText("Touchpad Debug View 1"); textView1.setGravity(Gravity.CENTER); @@ -98,7 +105,7 @@ public class TouchpadDebugView extends LinearLayout { textView1.setLayoutParams(new LayoutParams(1000, 200)); TextView textView2 = new TextView(context); - textView2.setBackgroundColor(Color.BLUE); + textView2.setBackgroundColor(Color.TRANSPARENT); textView2.setTextSize(20); textView2.setText("Touchpad Debug View 2"); textView2.setGravity(Gravity.CENTER); @@ -126,9 +133,7 @@ public class TouchpadDebugView extends LinearLayout { case MotionEvent.ACTION_MOVE: deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; - Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop); if (isSlopExceeded(deltaX, deltaY)) { - Slog.d("TouchpadDebugView", "Slop exceeded"); mWindowLayoutParams.x = Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX), mScreenWidth - this.getWidth())); @@ -136,9 +141,6 @@ public class TouchpadDebugView extends LinearLayout { Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY), mScreenHeight - this.getHeight())); - Slog.d("TouchpadDebugView", "New position X: " - + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y); - mWindowManager.updateViewLayout(this, mWindowLayoutParams); } return true; @@ -166,7 +168,7 @@ public class TouchpadDebugView extends LinearLayout { @Override public boolean performClick() { super.performClick(); - Slog.d("TouchpadDebugView", "You clicked me!"); + Slog.d("TouchpadDebugView", "You tapped the window!"); return true; } @@ -201,4 +203,34 @@ public class TouchpadDebugView extends LinearLayout { public WindowManager.LayoutParams getWindowLayoutParams() { return mWindowLayoutParams; } + + public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) { + if (mLastTouchpadState.getButtonsDown() == 0) { + if (touchpadHardwareState.getButtonsDown() > 0) { + onTouchpadButtonPress(); + } + } else { + if (touchpadHardwareState.getButtonsDown() == 0) { + onTouchpadButtonRelease(); + } + } + mLastTouchpadState = touchpadHardwareState; + } + + private void onTouchpadButtonPress() { + Slog.d("TouchpadDebugView", "You clicked me!"); + + // Iterate through all child views + // Temporary demonstration for testing + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setBackgroundColor(Color.BLUE); + } + } + + private void onTouchpadButtonRelease() { + Slog.d("TouchpadDebugView", "You released the click"); + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setBackgroundColor(Color.RED); + } + } } diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java index c28e74a02071..bc53c4947a71 100644 --- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java @@ -27,6 +27,7 @@ import android.view.WindowManager; import com.android.server.input.InputManagerService; import com.android.server.input.TouchpadHardwareProperties; +import com.android.server.input.TouchpadHardwareState; import java.util.Objects; @@ -132,4 +133,10 @@ public class TouchpadDebugViewController implements InputManager.InputDeviceList mTouchpadDebugView = null; Slog.d(TAG, "Touchpad debug view removed."); } + + public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) { + if (mTouchpadDebugView != null) { + mTouchpadDebugView.updateHardwareState(touchpadHardwareState); + } + } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index af0ccf93c826..f7478799527c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -4678,6 +4678,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } + // TODO(b/356239178): Make dump proto multi-user aware. private void dumpDebug(ProtoOutputStream proto, long fieldId) { synchronized (ImfLock.class) { final int userId = mCurrentImeUserId; @@ -6103,17 +6104,40 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. @BinderThread private void dumpAsStringNoCheck(FileDescriptor fd, PrintWriter pw, String[] args, boolean isCritical) { - IInputMethodInvoker method; - ClientState client; - + final int argUserId = parseUserIdFromDumpArgs(args); final Printer p = new PrintWriterPrinter(pw); + p.println("Current Input Method Manager state:"); + p.println(" concurrentMultiUserModeEnabled=" + mConcurrentMultiUserModeEnabled); + if (mConcurrentMultiUserModeEnabled && argUserId == UserHandle.USER_NULL) { + mUserDataRepository.forAllUserData( + u -> dumpAsStringNoCheckForUser(u, fd, pw, args, isCritical)); + } else { + final int userId = argUserId != UserHandle.USER_NULL ? argUserId : mCurrentImeUserId; + final var userData = getUserData(userId); + dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical); + } + } + + @UserIdInt + private static int parseUserIdFromDumpArgs(String[] args) { + final int userIdx = Arrays.binarySearch(args, "--user"); + if (userIdx == -1 || userIdx == args.length - 1) { + return UserHandle.USER_NULL; + } + return Integer.parseInt(args[userIdx + 1]); + } + // TODO(b/356239178): Update dump format output to better group per-user info. + @BinderThread + private void dumpAsStringNoCheckForUser(UserData userData, FileDescriptor fd, PrintWriter pw, + String[] args, boolean isCritical) { + final Printer p = new PrintWriterPrinter(pw); + IInputMethodInvoker method; + ClientState client; + p.println(" UserId=" + userData.mUserId); synchronized (ImfLock.class) { - final int userId = mCurrentImeUserId; - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final var userData = getUserData(userId); - p.println("Current Input Method Manager state:"); - p.println(" concurrentMultiUserModeEnabled" + mConcurrentMultiUserModeEnabled); + final InputMethodSettings settings = InputMethodSettingsRepository.get( + userData.mUserId); final List<InputMethodInfo> methodList = settings.getMethodList(); int numImes = methodList.size(); p.println(" Input Methods:"); @@ -6133,7 +6157,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println(" sessionRequested=" + c.mSessionRequested); p.println(" sessionRequestedForAccessibility=" - + c.mSessionRequestedForAccessibility); + + c.mSessionRequestedForAccessibility); p.println(" curSession=" + c.mCurSession); p.println(" selfReportedDisplayId=" + c.mSelfReportedDisplayId); p.println(" uid=" + c.mUid); @@ -6141,7 +6165,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. }; mClientController.forAllClients(clientControllerDump); final var bindingController = userData.mBindingController; - p.println(" mCurrentImeUserId=" + userData.mUserId); p.println(" mCurMethodId=" + bindingController.getSelectedMethodId()); client = userData.mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" @@ -6234,8 +6257,6 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. p.println("No input method client."); } synchronized (ImfLock.class) { - final int userId = mCurrentImeUserId; - final var userData = getUserData(userId); if (userData.mImeBindingState.mFocusedWindowClient != null && client != userData.mImeBindingState.mFocusedWindowClient) { p.println(" "); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 4fcf27d62a1a..7d44ba199119 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -889,8 +889,14 @@ public class LockSettingsService extends ILockSettings.Stub { // Hide notification first, as tie profile lock takes time hideEncryptionNotification(new UserHandle(userId)); - if (isCredentialSharableWithParent(userId)) { - tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) { + synchronized (mSpManager) { + tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + } + } else { + if (isCredentialSharableWithParent(userId)) { + tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + } } } }); @@ -1287,7 +1293,13 @@ public class LockSettingsService extends ILockSettings.Stub { mStorage.removeChildProfileLock(userId); removeKeystoreProfileKey(userId); } else { - tieProfileLockIfNecessary(userId, profileUserPassword); + if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) { + synchronized (mSpManager) { + tieProfileLockIfNecessary(userId, profileUserPassword); + } + } else { + tieProfileLockIfNecessary(userId, profileUserPassword); + } } } catch (IllegalStateException e) { setBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, old, userId); @@ -3403,8 +3415,13 @@ public class LockSettingsService extends ILockSettings.Stub { // It's OK to dump the credential type since anyone with physical access can just // observe it from the keyguard directly. pw.println("Quality: " + getKeyguardStoredQuality(userId)); - pw.println("CredentialType: " + LockPatternUtils.credentialTypeToString( - getCredentialTypeInternal(userId))); + final int credentialType = getCredentialTypeInternal(userId); + pw.println("CredentialType: " + + LockPatternUtils.credentialTypeToString(credentialType)); + if (credentialType == CREDENTIAL_TYPE_NONE) { + pw.println("IsLockScreenDisabled: " + + getBoolean(LockPatternUtils.DISABLE_LOCKSCREEN_KEY, false, userId)); + } pw.println("SeparateChallenge: " + getSeparateProfileChallengeEnabledInternal(userId)); pw.println(TextUtils.formatSimple("Metrics: %s", getUserPasswordMetrics(userId) != null ? "known" : "unknown")); diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index bad959af7aad..925ba1752fe2 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -22,6 +22,7 @@ import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static com.android.server.notification.ZenLog.traceApplyDeviceEffect; import static com.android.server.notification.ZenLog.traceScheduleApplyDeviceEffect; +import android.app.KeyguardManager; import android.app.UiModeManager; import android.app.WallpaperManager; import android.content.BroadcastReceiver; @@ -53,6 +54,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { private final Context mContext; private final ColorDisplayManager mColorDisplayManager; + private final KeyguardManager mKeyguardManager; private final PowerManager mPowerManager; private final UiModeManager mUiModeManager; private final WallpaperManager mWallpaperManager; @@ -67,6 +69,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { DefaultDeviceEffectsApplier(Context context) { mContext = context; mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + mKeyguardManager = context.getSystemService(KeyguardManager.class); mPowerManager = context.getSystemService(PowerManager.class); mUiModeManager = context.getSystemService(UiModeManager.class); WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class); @@ -133,12 +136,14 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { // Changing the theme can be disruptive for the user (Activities are likely recreated, may // lose some state). Therefore we only apply the change immediately if the rule was - // activated manually, or we are initializing, or the screen is currently off/dreaming. + // activated manually, or we are initializing, or the screen is currently off/dreaming, + // or if the device is locked. if (origin == ZenModeConfig.ORIGIN_INIT || origin == ZenModeConfig.ORIGIN_INIT_USER || origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI || origin == ZenModeConfig.ORIGIN_USER_IN_APP - || !mPowerManager.isInteractive()) { + || !mPowerManager.isInteractive() + || (android.app.Flags.modesUi() && mKeyguardManager.isKeyguardLocked())) { unregisterScreenOffReceiver(); updateNightModeImmediately(useNightMode); } else { diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 0cc50e68ea21..03fc60cad8d6 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -103,7 +103,7 @@ import java.util.Set; * - A remote interface definition (aidl) provided by the service used for communication. */ abstract public class ManagedServices { - protected final String TAG = getClass().getSimpleName(); + protected final String TAG = getClass().getSimpleName().replace('$', '.'); protected final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000; @@ -1094,7 +1094,7 @@ abstract public class ManagedServices { return info; } throw new SecurityException("Disallowed call from unknown " + getCaption() + ": " - + service + " " + service.getClass()); + + service.asBinder() + " " + service.getClass()); } public boolean isSameUser(IInterface service, int userId) { @@ -1585,6 +1585,9 @@ abstract public class ManagedServices { // after the rebind delay if (isPackageOrComponentAllowedWithPermission(cn, userId)) { registerService(cn, userId); + } else { + if (DEBUG) Slog.v(TAG, "skipped reregisterService cn=" + cn + " u=" + userId + + " because of isPackageOrComponentAllowedWithPermission check"); } } @@ -1918,6 +1921,7 @@ abstract public class ManagedServices { .append(",targetSdkVersion=").append(targetSdkVersion) .append(",connection=").append(connection == null ? null : "<connection>") .append(",service=").append(service) + .append(",serviceAsBinder=").append(service != null ? service.asBinder() : null) .append(']').toString(); } @@ -1956,7 +1960,7 @@ abstract public class ManagedServices { @Override public void binderDied() { - if (DEBUG) Slog.d(TAG, "binderDied"); + if (DEBUG) Slog.d(TAG, "binderDied " + this); // Remove the service, but don't unbind from the service. The system will bring the // service back up, and the onServiceConnected handler will read the service with the // new binding. If this isn't a bound service, and is just a registered diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 43a285cba4b9..f449126af0f9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1882,10 +1882,16 @@ final class InstallPackageHelper { } if (!oldSharedUid.equals(newSharedUid)) { - throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, - "Package " + parsedPackage.getPackageName() - + " shared user changed from " - + oldSharedUid + " to " + newSharedUid); + if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null + && ps.isArchivedOnAnyUser(allUsers))) { + // Only allow changing sharedUserId if unarchiving + // TODO(b/361558423): remove this check after pre-archiving installs + // accept a sharedUserId param in the API + throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, + "Package " + parsedPackage.getPackageName() + + " shared user changed from " + + oldSharedUid + " to " + newSharedUid); + } } // APK should not re-join shared UID diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 5e45b4c2d5af..76ea0b963036 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -283,10 +283,7 @@ public class PackageArchiver { return START_CLASS_NOT_FOUND; } - String currentLauncherPackageName = getCurrentLauncherPackageName(getParentUserId(userId)); - if ((currentLauncherPackageName == null || !TextUtils.equals(callerPackageName, - currentLauncherPackageName)) && callingUid != Process.SHELL_UID) { - // TODO(b/311619990): Remove dependency on SHELL_UID for testing + if (!isCallerQualifiedForUnarchival(callerPackageName, callingUid, userId)) { Slog.e(TAG, TextUtils.formatSimple( "callerPackageName: %s does not qualify for unarchival of package: " + "%s!", callerPackageName, packageName)); @@ -335,6 +332,37 @@ public class PackageArchiver { return START_ABORTED; } + private boolean isCallerQualifiedForUnarchival(String callerPackageName, int callingUid, + int userId) { + // TODO(b/311619990): Remove dependency on SHELL_UID for testing + if (callingUid == Process.SHELL_UID) { + return true; + } + String currentLauncherPackageName = getCurrentLauncherPackageName(getParentUserId(userId)); + if (currentLauncherPackageName != null && TextUtils.equals( + callerPackageName, currentLauncherPackageName)) { + return true; + } + Slog.w(TAG, TextUtils.formatSimple( + "Requester of unarchival: %s is not the default launcher package: %s.", + callerPackageName, currentLauncherPackageName)); + // When the default launcher is not set, or when the current caller is not the default + // launcher, allow the caller to directly request unarchive if it is a launcher app + // that is a pre-installed system app. + final Computer snapshot = mPm.snapshotComputer(); + final PackageStateInternal ps = snapshot.getPackageStateInternal(callerPackageName); + final boolean isSystem = ps != null && ps.isSystem(); + return isSystem && isLauncherApp(snapshot, callerPackageName, userId); + } + + private boolean isLauncherApp(Computer snapshot, String packageName, int userId) { + final Intent intent = snapshot.getHomeIntent(); + intent.setPackage(packageName); + List<ResolveInfo> launcherActivities = snapshot.queryIntentActivitiesInternal( + intent, null /* resolvedType */, 0 /* flags */, userId); + return !launcherActivities.isEmpty(); + } + // Profiles share their UI and default apps, so we have to get the profile parent before // fetching the default launcher. private int getParentUserId(int userId) { @@ -936,13 +964,9 @@ public class PackageArchiver { * <p> In the rare case the app had multiple launcher activities, only one of the icons is * returned arbitrarily. * - * <p> By default, the icon will be overlay'd with a cloud icon on top. A launcher app can + * <p> By default, the icon will be overlay'd with a cloud icon on top. An app can * disable the cloud overlay via the * {@link LauncherApps.ArchiveCompatibilityParams#setEnableIconOverlay(boolean)} API. - * The default launcher's cloud overlay mode determines the cloud overlay status returned by - * any other callers. That is, if the current launcher has the cloud overlay disabled, any other - * app that fetches the app icon will also get an icon that has the cloud overlay disabled. - * This is to prevent style mismatch caused by icons that are fetched by different callers. */ @Nullable public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9fb9e717fe4d..9428de700385 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -925,6 +925,18 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return PackageArchiver.isArchived(readUserState(userId)); } + /** + * @return if the package is archived in any of the users + */ + boolean isArchivedOnAnyUser(int[] userIds) { + for (int user : userIds) { + if (isArchived(user)) { + return true; + } + } + return false; + } + int getInstallReason(int userId) { return readUserState(userId).getInstallReason(); } diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 1c70af0a56ea..5ca5fda19b2f 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -397,6 +397,8 @@ public class PackageInfoUtils { ai.privateFlags |= flag(state.isInstantApp(), ApplicationInfo.PRIVATE_FLAG_INSTANT) | flag(state.isVirtualPreload(), ApplicationInfo.PRIVATE_FLAG_VIRTUAL_PRELOAD) | flag(state.isHidden(), ApplicationInfo.PRIVATE_FLAG_HIDDEN); + ai.privateFlagsExt |= + flag(state.isNotLaunched(), ApplicationInfo.PRIVATE_FLAG_EXT_NOT_LAUNCHED); if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { ai.enabled = true; } else if (state.getEnabledState() diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 749025b0cf40..ed9dcfadab83 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -535,7 +535,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { volatile boolean mRequestedOrSleepingDefaultDisplay; /** - * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is + * This is used to check whether to acquire screen-off sleep token when screen is * turned off. E.g. if it is false when screen is turned off and the display is swapping, it * is expected that the screen will be on in a short time. Then it is unnecessary to acquire * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes. @@ -610,7 +610,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { private boolean mPendingKeyguardOccluded; private boolean mKeyguardOccludedChanged; - private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer; Intent mHomeIntent; Intent mCarDockIntent; Intent mDeskDockIntent; @@ -810,7 +809,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { event.recycle(); break; case MSG_HANDLE_ALL_APPS: - launchAllAppsAction(); + launchAllAppsAction((KeyEvent) msg.obj); break; case MSG_RINGER_TOGGLE_CHORD: handleRingerChordGesture(); @@ -1879,26 +1878,31 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - private void launchAllAppsAction() { - Intent intent = new Intent(Intent.ACTION_ALL_APPS); - if (mHasFeatureLeanback) { - Intent intentLauncher = new Intent(Intent.ACTION_MAIN); - intentLauncher.addCategory(Intent.CATEGORY_HOME); - ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(intentLauncher, - PackageManager.MATCH_SYSTEM_ONLY, - mCurrentUserId); - if (resolveInfo != null) { - intent.setPackage(resolveInfo.activityInfo.packageName); + private void launchAllAppsAction(KeyEvent event) { + if (mHasFeatureLeanback || mHasFeatureWatch) { + // TV and watch support the all apps intent + notifyKeyGestureCompleted(event, + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS); + Intent intent = new Intent(Intent.ACTION_ALL_APPS); + if (mHasFeatureLeanback) { + Intent intentLauncher = new Intent(Intent.ACTION_MAIN); + intentLauncher.addCategory(Intent.CATEGORY_HOME); + ResolveInfo resolveInfo = mPackageManager.resolveActivityAsUser(intentLauncher, + PackageManager.MATCH_SYSTEM_ONLY, + mCurrentUserId); + if (resolveInfo != null) { + intent.setPackage(resolveInfo.activityInfo.packageName); + } + } + startActivityAsUser(intent, UserHandle.CURRENT); + } else { + notifyKeyGestureCompleted(event, + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS); + AccessibilityManagerInternal accessibilityManager = getAccessibilityManagerInternal(); + if (accessibilityManager != null) { + accessibilityManager.performSystemAction( + AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); } - } - startActivityAsUser(intent, UserHandle.CURRENT); - } - - private void launchAllAppsViaA11y() { - AccessibilityManagerInternal accessibilityManager = getAccessibilityManagerInternal(); - if (accessibilityManager != null) { - accessibilityManager.performSystemAction( - AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); } dismissKeyboardShortcutsMenu(); } @@ -2076,15 +2080,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Home - Long Press"); switch (mLongPressOnHomeBehavior) { case LONG_PRESS_HOME_ALL_APPS: - if (mHasFeatureLeanback) { - launchAllAppsAction(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS); - } else { - launchAllAppsViaA11y(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS); - } + launchAllAppsAction(event); break; case LONG_PRESS_HOME_ASSIST: notifyKeyGestureCompleted(event, @@ -2223,9 +2219,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mLockPatternUtils = new LockPatternUtils(mContext); mLogger = new MetricsLogger(); - mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal - .createSleepTokenAcquirer("ScreenOff"); - Resources res = mContext.getResources(); mWakeOnDpadKeyPress = res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress); @@ -3695,18 +3688,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { break; case KeyEvent.KEYCODE_ALL_APPS: if (firstDown) { - if (mHasFeatureLeanback) { - mHandler.removeMessages(MSG_HANDLE_ALL_APPS); - Message msg = mHandler.obtainMessage(MSG_HANDLE_ALL_APPS); - msg.setAsynchronous(true); - msg.sendToTarget(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS); - } else { - launchAllAppsViaA11y(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS); - } + mHandler.removeMessages(MSG_HANDLE_ALL_APPS); + + Message msg = mHandler.obtainMessage(MSG_HANDLE_ALL_APPS, new KeyEvent(event)); + msg.setAsynchronous(true); + msg.sendToTarget(); } return true; case KeyEvent.KEYCODE_NOTIFICATION: @@ -3759,9 +3745,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK); } else if (mPendingMetaAction) { if (!canceled) { - launchAllAppsViaA11y(); - notifyKeyGestureCompleted(event, - KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS); + launchAllAppsAction(event); } mPendingMetaAction = false; } @@ -5533,13 +5517,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { mRequestedOrSleepingDefaultDisplay = true; mIsGoingToSleepDefaultDisplay = true; - // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in - // order but the methods run on different threads) and updateScreenOffSleepToken was - // skipped. Then acquire sleep token if screen was off. - if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()) { - updateScreenOffSleepToken(true /* acquire */); - } - if (mKeyguardDelegate != null) { mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason); } @@ -5700,11 +5677,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off..."); if (displayId == DEFAULT_DISPLAY) { - if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay) { - updateScreenOffSleepToken(true /* acquire */); - } + final boolean acquireSleepToken = !isSwappingDisplay || mIsGoingToSleepDefaultDisplay; mRequestedOrSleepingDefaultDisplay = false; - mDefaultDisplayPolicy.screenTurnedOff(); + mDefaultDisplayPolicy.screenTurnedOff(acquireSleepToken); synchronized (mLock) { if (mKeyguardDelegate != null) { mKeyguardDelegate.onScreenTurnedOff(); @@ -5760,7 +5735,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (displayId == DEFAULT_DISPLAY) { Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn", 0 /* cookie */); - updateScreenOffSleepToken(false /* acquire */); mDefaultDisplayPolicy.screenTurningOn(screenOnListener); mBootAnimationDismissable = false; @@ -6267,15 +6241,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - // TODO (multidisplay): Support multiple displays in WindowManagerPolicy. - private void updateScreenOffSleepToken(boolean acquire) { - if (acquire) { - mScreenOffSleepTokenAcquirer.acquire(DEFAULT_DISPLAY); - } else { - mScreenOffSleepTokenAcquirer.release(DEFAULT_DISPLAY); - } - } - /** {@inheritDoc} */ @Override public void enableScreenAfterBoot() { diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 12e7fd010e3d..71cb8820761f 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -3668,7 +3668,7 @@ public final class PowerManagerService extends SystemService mBrightWhenDozingConfig); int wakefulness = powerGroup.getWakefulnessLocked(); if (DEBUG_SPEW) { - Slog.d(TAG, "updateDisplayPowerStateLocked: displayReady=" + ready + Slog.d(TAG, "updatePowerGroupsLocked: displayReady=" + ready + ", groupId=" + groupId + ", policy=" + policyToString(powerGroup.getPolicyLocked()) + ", mWakefulness=" diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 09d2a0263f2e..83cb72e1df1f 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -35,7 +35,8 @@ import com.android.server.notification.NotificationDelegate; public interface StatusBarManagerInternal { void setNotificationDelegate(NotificationDelegate delegate); - void showScreenPinningRequest(int taskId); + /** Show a screen pinning request for a specific task. */ + void showScreenPinningRequest(int taskId, int userId); void showAssistDisclosure(); void preloadRecentApps(); @@ -136,7 +137,7 @@ public interface StatusBarManagerInternal { * * @param hidesStatusBar whether it is being hidden */ - void setTopAppHidesStatusBar(boolean hidesStatusBar); + void setTopAppHidesStatusBar(int displayId, boolean hidesStatusBar); boolean showShutdownUi(boolean isReboot, String requestString); @@ -149,17 +150,18 @@ public interface StatusBarManagerInternal { /** * Notify System UI that the system get into or exit immersive mode. + * @param displayId The changed display Id. * @param rootDisplayAreaId The changed display area Id. * @param isImmersiveMode {@code true} if the display area get into immersive mode. */ - void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode); + void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode); /** * Show a rotation suggestion that a user may approve to rotate the screen. * * @param rotation rotation suggestion */ - void onProposedRotationChanged(int rotation, boolean isValid); + void onProposedRotationChanged(int displayId, int rotation, boolean isValid); /** * Notifies System UI that the display is ready to show system decorations. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 0fd59670436e..908f51b9cba9 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -119,6 +119,7 @@ import com.android.server.LocalServices; import com.android.server.UiThread; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.notification.NotificationDelegate; +import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.policy.GlobalActionsProvider; import com.android.server.power.ShutdownCheckPoints; @@ -185,6 +186,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private final ActivityManagerInternal mActivityManagerInternal; private final ActivityTaskManagerInternal mActivityTaskManager; private final PackageManagerInternal mPackageManagerInternal; + private final UserManagerInternal mUserManagerInternal; private final SessionMonitor mSessionMonitor; private int mCurrentUserId; private boolean mTracingEnabled; @@ -304,6 +306,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D mActivityTaskManager = LocalServices.getService(ActivityTaskManagerInternal.class); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mTileRequestTracker = new TileRequestTracker(mContext); mSessionMonitor = new SessionMonitor(mContext); @@ -360,7 +363,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void showScreenPinningRequest(int taskId) { + public void showScreenPinningRequest(int taskId, int userId) { + if (isVisibleBackgroundUser(userId)) { + if (SPEW) { + Slog.d(TAG, "Skipping showScreenPinningRequest for visible background user " + + userId); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -439,6 +449,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void appTransitionFinished(int displayId) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping appTransitionFinished for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } enforceStatusBarService(); IStatusBar bar = mBar; if (bar != null) { @@ -588,6 +605,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void setWindowState(int displayId, int window, int state) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping setWindowState for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -598,6 +622,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void appTransitionPending(int displayId) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping appTransitionPending for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -608,6 +639,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void appTransitionCancelled(int displayId) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping appTransitionCancelled for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -619,6 +657,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void appTransitionStarting(int displayId, long statusBarAnimationsStartTime, long statusBarAnimationsDuration) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping appTransitionStarting for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -629,7 +674,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void setTopAppHidesStatusBar(boolean hidesStatusBar) { + public void setTopAppHidesStatusBar(int displayId, boolean hidesStatusBar) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping setTopAppHidesStatusBar for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -665,10 +717,18 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + public void immersiveModeChanged(int displayId, int rootDisplayAreaId, + boolean isImmersiveMode) { if (mBar == null) { return; } + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping immersiveModeChanged for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } if (!CLIENT_TRANSIENT) { // Only call from here when the client transient is not enabled. try { @@ -680,7 +740,14 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D // TODO(b/118592525): support it per display if necessary. @Override - public void onProposedRotationChanged(int rotation, boolean isValid) { + public void onProposedRotationChanged(int displayId, int rotation, boolean isValid) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping onProposedRotationChanged for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } if (mBar != null){ try { mBar.onProposedRotationChanged(rotation, isValid); @@ -690,6 +757,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void onDisplayReady(int displayId) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping onDisplayReady for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -703,6 +777,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D AppearanceRegion[] appearanceRegions, boolean navbarColorManagedByIme, @Behavior int behavior, @InsetsType int requestedVisibleTypes, String packageName, LetterboxDetails[] letterboxDetails) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping onSystemBarAttributesChanged for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } getUiState(displayId).setBarAttributes(appearance, appearanceRegions, navbarColorManagedByIme, behavior, requestedVisibleTypes, packageName, letterboxDetails); @@ -719,6 +800,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void showTransient(int displayId, @InsetsType int types, boolean isGestureOnSystemBar) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping showTransient for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } getUiState(displayId).showTransient(types); IStatusBar bar = mBar; if (bar != null) { @@ -730,6 +818,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void abortTransient(int displayId, @InsetsType int types) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping abortTransient for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } getUiState(displayId).clearTransient(types); IStatusBar bar = mBar; if (bar != null) { @@ -776,6 +871,15 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, + "Skipping setNavigationBarLumaSamplingEnabled for visible background " + + "user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } IStatusBar bar = mBar; if (bar != null) { try { @@ -1416,6 +1520,13 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } private void setDisableFlags(int displayId, int flags, String cause) { + if (isVisibleBackgroundUserOnDisplay(displayId)) { + if (SPEW) { + Slog.d(TAG, "Skipping setDisableFlags for visible background user " + + mUserManagerInternal.getUserAssignedToDisplay(displayId)); + } + return; + } // also allows calls from window manager which is in this process. enforceStatusBarService(); @@ -2713,16 +2824,30 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D if (callingUserId == USER_SYSTEM || callingUserId == mCurrentUserId) { return; } - final long ident = Binder.clearCallingIdentity(); - try { - if (mUserManager.isSameProfileGroup(callingUserId, mCurrentUserId)) { - return; - } - } finally { - Binder.restoreCallingIdentity(ident); + if (!isVisibleBackgroundUser(callingUserId)) { + return; } throw new SecurityException("User " + callingUserId + " is not permitted to use this method"); } -} + + private boolean isVisibleBackgroundUser(int userId) { + if (!mVisibleBackgroundUsersEnabled) { + return false; + } + // The main use case for visible background users is the Automotive multi-display + // configuration where a passenger can use a secondary display while the driver is + // using the main display. + // TODO(b/341604160) - Support visible background users properly and remove carve outs + return mUserManagerInternal.isVisibleBackgroundFullUser(userId); + } + + private boolean isVisibleBackgroundUserOnDisplay(int displayId) { + if (!mVisibleBackgroundUsersEnabled) { + return false; + } + int userId = mUserManagerInternal.getUserAssignedToDisplay(displayId); + return isVisibleBackgroundUser(userId); + } +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index 474253223628..5f704a002a33 100644 --- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -276,7 +276,7 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { // enabled on the last one as a sample mInboundTransform = inboundTransform; - if (!Flags.allowDisableIpsecLossDetector() || canStart()) { + if (canStart()) { start(); } } @@ -292,7 +292,7 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig); } - if (Flags.allowDisableIpsecLossDetector() && canStart() != isStarted()) { + if (canStart() != isStarted()) { if (canStart()) { start(); } else { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index f53dda6ee35b..4dcc6e112ecc 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3169,7 +3169,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final WallpaperDestinationChangeHandler liveSync = new WallpaperDestinationChangeHandler( newWallpaper); - boolean same = changingToSame(name, newWallpaper); + boolean same = changingToSame(name, newWallpaper.connection, + newWallpaper.wallpaperComponent); /* * If we have a shared system+lock wallpaper, and we reapply the same wallpaper @@ -3257,14 +3258,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return name == null || name.equals(mDefaultWallpaperComponent); } - private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) { - if (wallpaper.connection != null) { - final ComponentName wallpaperName = wallpaper.wallpaperComponent; - if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) { + private boolean changingToSame(ComponentName newComponentName, + WallpaperConnection currentConnection, ComponentName currentComponentName) { + if (currentConnection != null) { + if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) { if (DEBUG) Slog.v(TAG, "changingToSame: still using default"); // Still using default wallpaper. return true; - } else if (wallpaperName != null && wallpaperName.equals(componentName)) { + } else if (currentComponentName != null && currentComponentName.equals( + newComponentName)) { // Changing to same wallpaper. if (DEBUG) Slog.v(TAG, "same wallpaper"); return true; @@ -3279,7 +3281,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName); } // Has the component changed? - if (!force && changingToSame(componentName, wallpaper)) { + if (!force && changingToSame(componentName, wallpaper.connection, + wallpaper.wallpaperComponent)) { try { if (DEBUG_LIVE) { Slog.v(TAG, "Changing to the same component, ignoring"); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 99747e05e7f0..0be6471f189e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8149,7 +8149,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ @Override protected int getOverrideOrientation() { - if (mWmService.mConstants.mIgnoreActivityOrientationRequest) { + if (mWmService.mConstants.mIgnoreActivityOrientationRequest + && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) { return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } return mAppCompatController.getOrientationPolicy() diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index b8ce02ed5937..3d6b64b2e536 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -130,35 +130,6 @@ public abstract class ActivityTaskManagerInternal { } /** - * Sleep tokens cause the activity manager to put the top activity to sleep. - * They are used by components such as dreams that may hide and block interaction - * with underlying activities. - * The Acquirer provides an interface that encapsulates the underlying work, so the user does - * not need to handle the token by him/herself. - */ - public interface SleepTokenAcquirer { - - /** - * Acquires a sleep token. - * @param displayId The display to apply to. - */ - void acquire(int displayId); - - /** - * Releases the sleep token. - * @param displayId The display to apply to. - */ - void release(int displayId); - } - - /** - * Creates a sleep token acquirer for the specified display with the specified tag. - * - * @param tag A string identifying the purpose (eg. "Dream"). - */ - public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag); - - /** * Returns home activity for the specified user. * * @param userId ID of the user or {@link android.os.UserHandle#USER_ALL} diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index e25d940d9781..49ca698e36e2 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4356,6 +4356,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mTaskOrganizerController.dump(pw, " "); mVisibleActivityProcessTracker.dump(pw, " "); mActiveUids.dump(pw, " "); + pw.println(" SleepTokens=" + mRootWindowContainer.mSleepTokens); if (mDemoteTopAppReasons != 0) { pw.println(" mDemoteTopAppReasons=" + mDemoteTopAppReasons); } @@ -5071,17 +5072,16 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason); } - final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer { + final class SleepTokenAcquirer { private final String mTag; private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens = new SparseArray<>(); - SleepTokenAcquirerImpl(@NonNull String tag) { + SleepTokenAcquirer(@NonNull String tag) { mTag = tag; } - @Override - public void acquire(int displayId) { + void acquire(int displayId) { synchronized (mGlobalLock) { if (!mSleepTokens.contains(displayId)) { mSleepTokens.append(displayId, @@ -5091,8 +5091,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } - @Override - public void release(int displayId) { + void release(int displayId) { synchronized (mGlobalLock) { final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId); if (token != null) { @@ -5955,11 +5954,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } final class LocalService extends ActivityTaskManagerInternal { - @Override - public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) { - Objects.requireNonNull(tag); - return new SleepTokenAcquirerImpl(tag); - } @Override public ComponentName getHomeActivityForUser(int userId) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e8a3951a93d4..10e0641b0582 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -735,8 +735,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** All tokens used to put activities on this root task to sleep (including mOffToken) */ final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>(); - /** The token acquirer to put root tasks on the display to sleep */ - private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer; private boolean mSleeping; @@ -1131,7 +1129,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplay = display; mDisplayId = display.getDisplayId(); mCurrentUniqueDisplayId = display.getUniqueId(); - mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer; mWallpaperController = new WallpaperController(mWmService, this); mWallpaperController.resetLargestDisplay(display); display.getDisplayInfo(mDisplayInfo); @@ -6157,9 +6154,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final int displayState = mDisplayInfo.state; if (displayId != DEFAULT_DISPLAY) { if (displayState == Display.STATE_OFF) { - mOffTokenAcquirer.acquire(mDisplayId); + mRootWindowContainer.mDisplayOffTokenAcquirer.acquire(mDisplayId); } else if (displayState == Display.STATE_ON) { - mOffTokenAcquirer.release(mDisplayId); + mRootWindowContainer.mDisplayOffTokenAcquirer.release(mDisplayId); } ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Display %d state was (%d), is now (%d), so update " diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 5c621208c4db..0fa1a2138e35 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -804,6 +804,14 @@ public class DisplayPolicy { mAwake /* waiting */); if (!awake) { onDisplaySwitchFinished(); + // In case PhoneWindowManager's startedGoingToSleep is called after screenTurnedOff + // (the source caller is in order but the methods run on different threads) and + // updateScreenOffSleepToken was skipped by mIsGoingToSleepDefaultDisplay. Then + // acquire sleep token if screen is off. + if (!mScreenOnEarly && !mScreenOnFully && !mDisplayContent.isSleeping()) { + Slog.w(TAG, "Late acquire sleep token for " + mDisplayContent); + mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId); + } } } } @@ -851,6 +859,7 @@ public class DisplayPolicy { public void screenTurningOn(ScreenOnListener screenOnListener) { WindowProcessController visibleDozeUiProcess = null; synchronized (mLock) { + mService.mRoot.mDisplayOffTokenAcquirer.release(mDisplayContent.mDisplayId); mScreenOnEarly = true; mScreenOnFully = false; mKeyguardDrawComplete = false; @@ -875,8 +884,12 @@ public class DisplayPolicy { onDisplaySwitchFinished(); } - public void screenTurnedOff() { + /** It is called after {@link #screenTurningOn}. This runs on PowerManager's thread. */ + public void screenTurnedOff(boolean acquireSleepToken) { synchronized (mLock) { + if (acquireSleepToken) { + mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId); + } mScreenOnEarly = false; mScreenOnFully = false; mKeyguardDrawComplete = false; @@ -2504,7 +2517,7 @@ public class DisplayPolicy { if (getStatusBar() != null) { final StatusBarManagerInternal statusBar = getStatusBarManagerInternal(); if (statusBar != null) { - statusBar.setTopAppHidesStatusBar(topAppHidesStatusBar); + statusBar.setTopAppHidesStatusBar(getDisplayId(), topAppHidesStatusBar); } } @@ -2531,9 +2544,9 @@ public class DisplayPolicy { mService.mPolicy.isUserSetupComplete(), isNavBarEmpty(disableFlags)); } else { - // TODO (b/277290737): Move this to the client side, instead of using a proxy. - callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(rootDisplayAreaId, - isImmersiveMode)); + // TODO(b/277290737): Move this to the client side, instead of using a proxy. + callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(), + rootDisplayAreaId, isImmersiveMode)); } } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index 5200e820fc02..8c06cfecdc40 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -81,7 +81,6 @@ import android.window.WindowContainerTransaction; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.server.LocalServices; import com.android.server.UiThread; import com.android.server.policy.WindowManagerPolicy; import com.android.server.statusbar.StatusBarManagerInternal; @@ -136,7 +135,6 @@ public class DisplayRotation { private final RotationLockHistory mRotationLockHistory = new RotationLockHistory(); private OrientationListener mOrientationListener; - private StatusBarManagerInternal mStatusBarManagerInternal; private SettingsObserver mSettingsObserver; @NonNull private final DeviceStateController mDeviceStateController; @@ -1559,11 +1557,9 @@ public class DisplayRotation { /** Notify the StatusBar that system rotation suggestion has changed. */ private void sendProposedRotationChangeToStatusBarInternal(int rotation, boolean isValid) { - if (mStatusBarManagerInternal == null) { - mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); - } - if (mStatusBarManagerInternal != null) { - mStatusBarManagerInternal.onProposedRotationChanged(rotation, isValid); + final StatusBarManagerInternal bar = mDisplayPolicy.getStatusBarManagerInternal(); + if (bar != null) { + bar.onProposedRotationChanged(mDisplayContent.getDisplayId(), rotation, isValid); } } diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index 5d8a96c530ef..0c489d6207e9 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -87,7 +87,7 @@ class KeyguardController { private final SparseArray<KeyguardDisplayState> mDisplayStates = new SparseArray<>(); private final ActivityTaskManagerService mService; private RootWindowContainer mRootWindowContainer; - private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer; + private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer; private boolean mWaitingForWakeTransition; private Transition.ReadyCondition mWaitAodHide = null; @@ -95,7 +95,7 @@ class KeyguardController { ActivityTaskSupervisor taskSupervisor) { mService = service; mTaskSupervisor = taskSupervisor; - mSleepTokenAcquirer = mService.new SleepTokenAcquirerImpl(KEYGUARD_SLEEP_TOKEN_TAG); + mSleepTokenAcquirer = mService.new SleepTokenAcquirer(KEYGUARD_SLEEP_TOKEN_TAG); } void setWindowManager(WindowManagerService windowManager) { @@ -658,10 +658,10 @@ class KeyguardController { private boolean mRequestDismissKeyguard; private final ActivityTaskManagerService mService; - private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer; + private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer; KeyguardDisplayState(ActivityTaskManagerService service, int displayId, - ActivityTaskManagerInternal.SleepTokenAcquirer acquirer) { + ActivityTaskManagerService.SleepTokenAcquirer acquirer) { mService = service; mDisplayId = displayId; mSleepTokenAcquirer = acquirer; diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java index 0dadade38ddb..e65396e00b20 100644 --- a/services/core/java/com/android/server/wm/LockTaskController.java +++ b/services/core/java/com/android/server/wm/LockTaskController.java @@ -659,7 +659,7 @@ public class LockTaskController { StatusBarManagerInternal statusBarManager = LocalServices.getService( StatusBarManagerInternal.class); if (statusBarManager != null) { - statusBarManager.showScreenPinningRequest(task.mTaskId); + statusBarManager.showScreenPinningRequest(task.mTaskId, task.mUserId); } return; } else if (mLockTaskModeState == LOCK_TASK_MODE_PINNED) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 866dcd56ea91..8f5612c61e1c 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -215,7 +215,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> private static final String DISPLAY_OFF_SLEEP_TOKEN_TAG = "Display-off"; /** The token acquirer to put root tasks on the displays to sleep */ - final ActivityTaskManagerInternal.SleepTokenAcquirer mDisplayOffTokenAcquirer; + final ActivityTaskManagerService.SleepTokenAcquirer mDisplayOffTokenAcquirer; /** * The modes which affect which tasks are returned when calling @@ -450,7 +450,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> mService = service.mAtmService; mTaskSupervisor = mService.mTaskSupervisor; mTaskSupervisor.mRootWindowContainer = this; - mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG); + mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirer(DISPLAY_OFF_SLEEP_TOKEN_TAG); mDeviceStateController = new DeviceStateController(service.mContext, service.mGlobalLock); mDisplayRotationCoordinator = new DisplayRotationCoordinator(); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 5aa34d22f00f..92953e5a5041 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -17,6 +17,8 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO; +import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO; import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; @@ -206,7 +208,13 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr mOrganizerPid = pid; mAppThread = getAppThread(pid, mOrganizerUid); for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) { - mOrganizedTaskFragments.get(i).onTaskFragmentOrganizerRestarted(organizer); + final TaskFragment taskFragment = mOrganizedTaskFragments.get(i); + if (taskFragment.isAttached() + && taskFragment.getTopNonFinishingActivity() != null) { + taskFragment.onTaskFragmentOrganizerRestarted(organizer); + } else { + mOrganizedTaskFragments.remove(taskFragment); + } } try { mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/); @@ -575,8 +583,29 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } mCachedTaskFragmentOrganizerStates.remove(cachedState); - outSavedState.putAll(cachedState.mSavedState); cachedState.restore(organizer, pid); + outSavedState.putAll(cachedState.mSavedState); + + // Collect the organized TfInfo and TfParentInfo in the system. + final ArrayList<TaskFragmentInfo> infos = new ArrayList<>(); + final ArrayMap<Integer, Task> tasks = new ArrayMap<>(); + final int fragmentCount = cachedState.mOrganizedTaskFragments.size(); + for (int j = 0; j < fragmentCount; j++) { + final TaskFragment tf = cachedState.mOrganizedTaskFragments.get(j); + infos.add(tf.getTaskFragmentInfo()); + if (!tasks.containsKey(tf.getTask().mTaskId)) { + tasks.put(tf.getTask().mTaskId, tf.getTask()); + } + } + outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENTS_INFO, infos); + + final ArrayList<TaskFragmentParentInfo> parentInfos = new ArrayList<>(); + for (int j = tasks.size() - 1; j >= 0; j--) { + parentInfos.add(tasks.valueAt(j).getTaskFragmentParentInfo()); + } + outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO, + parentInfos); + mTaskFragmentOrganizerState.put(organizer.asBinder(), cachedState); mPendingTaskFragmentEvents.put(organizer.asBinder(), new ArrayList<>()); return true; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index e6226ab6786d..58dce445a3e3 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2333,10 +2333,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Place the nav bar on top of anything else in the top activity. t.setLayer(navSurfaceControl, Integer.MAX_VALUE); } - final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal(); - if (bar != null) { - bar.setNavigationBarLumaSamplingEnabled(mRecentsDisplayId, false); - } + sendLumaSamplingEnabledToStatusBarInternal(dc, false); } /** @see RecentsAnimationController#restoreNavigationBarFromApp */ @@ -2354,10 +2351,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { final DisplayContent dc = mController.mAtm.mRootWindowContainer.getDisplayContent(recentsDisplayId); - final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal(); - if (bar != null) { - bar.setNavigationBarLumaSamplingEnabled(recentsDisplayId, true); - } + sendLumaSamplingEnabledToStatusBarInternal(dc, true); final WindowState navWindow = dc.getDisplayPolicy().getNavigationBar(); if (navWindow == null) return; navWindow.setSurfaceTranslationY(0); @@ -2391,6 +2385,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { dc.mWmService.scheduleAnimationLocked(); } + private void sendLumaSamplingEnabledToStatusBarInternal(@NonNull DisplayContent dc, + boolean enabled) { + final StatusBarManagerInternal bar = dc.getDisplayPolicy().getStatusBarManagerInternal(); + if (bar != null) { + bar.setNavigationBarLumaSamplingEnabled(dc.getDisplayId(), enabled); + } + } + private void reportStartReasonsToLogger() { // Record transition start in metrics logger. We just assume everything is "DRAWN" // at this point since splash-screen is a presentation (shell) detail. diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java index 47c42f4292f1..e0f24d8bf447 100644 --- a/services/core/java/com/android/server/wm/WindowManagerConstants.java +++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java @@ -34,7 +34,7 @@ import java.util.concurrent.Executor; */ final class WindowManagerConstants { - /** The orientation of activity will be always "unspecified". */ + /** The orientation of activity will be always "unspecified" except for game apps. */ private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST = "ignore_activity_orientation_request"; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 85e24c15c1a5..b6e45fc803f7 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -14799,7 +14799,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } @Override - public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled) { + public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled, + PersistableBundle options) { Objects.requireNonNull(who, "ComponentName is null"); // Check can set secondary lockscreen enabled @@ -18644,11 +18645,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { toggleBackupServiceActive(caller.getUserId(), enabled); - if (Flags.backupServiceSecurityLogEventEnabled()) { - if (SecurityLog.isLoggingEnabled()) { - SecurityLog.writeEvent(SecurityLog.TAG_BACKUP_SERVICE_TOGGLED, - caller.getPackageName(), caller.getUserId(), enabled ? 1 : 0); - } + if (SecurityLog.isLoggingEnabled()) { + SecurityLog.writeEvent(SecurityLog.TAG_BACKUP_SERVICE_TOGGLED, + caller.getPackageName(), caller.getUserId(), enabled ? 1 : 0); } } diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp new file mode 100644 index 000000000000..b5cf98697d54 --- /dev/null +++ b/services/tests/appfunctions/Android.bp @@ -0,0 +1,59 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "FrameworksAppFunctionsTests", + team: "trendy_team_machine_learning", + defaults: [ + "modules-utils-testable-device-config-defaults", + ], + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "androidx.test.ext.truth", + "platform-test-annotations", + "services.appfunctions", + "servicestests-core-utils", + "truth", + "frameworks-base-testutils", + "androidx.test.rules", + ], + + libs: [ + "android.test.base", + "android.test.runner", + ], + + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], + + optimize: { + enabled: false, + }, +} diff --git a/services/tests/appfunctions/AndroidManifest.xml b/services/tests/appfunctions/AndroidManifest.xml new file mode 100644 index 000000000000..1d42b177db59 --- /dev/null +++ b/services/tests/appfunctions/AndroidManifest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.appfunctionstests"> + + <application android:testOnly="true" + android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.appfunctionstests" + android:label="Frameworks AppFunctions Services Tests" /> + +</manifest>
\ No newline at end of file diff --git a/services/tests/appfunctions/AndroidTest.xml b/services/tests/appfunctions/AndroidTest.xml new file mode 100644 index 000000000000..0650120afe24 --- /dev/null +++ b/services/tests/appfunctions/AndroidTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open 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 Frameworks AppFunctions Services Tests."> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="FrameworksAppFunctionsTests.apk" /> + </target_preparer> + + <option name="test-tag" value="FrameworksAppFunctionsTests" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.appfunctionstests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ No newline at end of file diff --git a/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionStaticMetadataHelperTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionStaticMetadataHelperTest.kt new file mode 100644 index 000000000000..d8ce39356d9c --- /dev/null +++ b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionStaticMetadataHelperTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.appfunctions + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class AppFunctionStaticMetadataHelperTest { + + @Test + fun getStaticSchemaNameForPackage() { + val actualSchemaName = + AppFunctionStaticMetadataHelper.getStaticSchemaNameForPackage("com.example.app") + + assertThat(actualSchemaName).isEqualTo("AppFunctionStaticMetadata-com.example.app") + } + + @Test + fun getDocumentIdForAppFunction() { + val packageName = "com.example.app" + val functionId = "someFunction" + + val actualDocumentId = + AppFunctionStaticMetadataHelper.getDocumentIdForAppFunction(packageName, functionId) + + assertThat(actualDocumentId).isEqualTo("com.example.app/someFunction") + } + + @Test + fun getStaticMetadataQualifiedId() { + val packageName = "com.example.app" + val functionId = "someFunction" + + val actualQualifiedId = + AppFunctionStaticMetadataHelper.getStaticMetadataQualifiedId(packageName, functionId) + + assertThat(actualQualifiedId) + .isEqualTo("android\$apps-db/app_functions#com.example.app/someFunction") + } +} diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt new file mode 100644 index 000000000000..a0f1a559bb52 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt @@ -0,0 +1,164 @@ +/* + * 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.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE +import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema +import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema +import android.app.appsearch.AppSearchManager +import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SearchSpec +import android.app.appsearch.SetSchemaRequest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FutureAppSearchSessionTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun setSchema() { + val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME) + ) + .build() + + val schema = session.setSchema(setSchemaRequest) + + assertThat(schema.get()).isNotNull() + } + } + + @Test + fun put() { + val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME) + ) + .build() + val schema = session.setSchema(setSchemaRequest) + assertThat(schema.get()).isNotNull() + val appFunctionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments(appFunctionRuntimeMetadata) + .build() + + val putResult = session.put(putDocumentsRequest) + + assertThat(putResult.get().isSuccess).isTrue() + } + } + + @Test + fun search() { + val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME) + ) + .build() + val schema = session.setSchema(setSchemaRequest) + assertThat(schema.get()).isNotNull() + val appFunctionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments(appFunctionRuntimeMetadata) + .build() + val putResult = session.put(putDocumentsRequest) + assertThat(putResult.get().isSuccess).isTrue() + + val searchResult = session.search("", SearchSpec.Builder().build()) + + val genericDocs = + searchResult.get().nextPage.get().stream().map { it.genericDocument }.toList() + assertThat(genericDocs).hasSize(1) + val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genericDocs[0]) + assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID) + } + } + + @Test + fun getByDocumentId() { + val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME) + ) + .build() + val schema = session.setSchema(setSchemaRequest) + val appFunctionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments(appFunctionRuntimeMetadata) + .build() + val putResult = session.put(putDocumentsRequest) + + val genricDocument = session + .getByDocumentId( + /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}", + APP_FUNCTION_RUNTIME_NAMESPACE + ) + .get() + + val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument) + assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID) + } + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_PACKAGE_NAME: String = "test_pkg" + const val TEST_FUNCTION_ID: String = "print" + } +} diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt new file mode 100644 index 000000000000..1fa55c7090aa --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema +import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema +import android.app.appsearch.AppSearchManager +import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SetSchemaRequest +import android.app.appsearch.observer.DocumentChangeInfo +import android.app.appsearch.observer.ObserverCallback +import android.app.appsearch.observer.ObserverSpec +import android.app.appsearch.observer.SchemaChangeInfo +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.infra.AndroidFuture +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FutureGlobalSearchSessionTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun registerDocumentChangeObserverCallback() { + val packageObserverSpec: ObserverSpec = + ObserverSpec.Builder() + .addFilterSchemas( + AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(TEST_TARGET_PKG_NAME) + ) + .build() + val settableDocumentChangeInfo: AndroidFuture<DocumentChangeInfo> = AndroidFuture() + val observer: ObserverCallback = + object : ObserverCallback { + override fun onSchemaChanged(changeInfo: SchemaChangeInfo) {} + + override fun onDocumentChanged(changeInfo: DocumentChangeInfo) { + settableDocumentChangeInfo.complete(changeInfo) + } + } + val futureGlobalSearchSession = FutureGlobalSearchSession(appSearchManager, testExecutor) + + val registerPackageObserver: Void? = + futureGlobalSearchSession + .registerObserverCallbackAsync( + TEST_TARGET_PKG_NAME, + packageObserverSpec, + testExecutor, + observer, + ) + .get() + assertThat(registerPackageObserver).isNull() + // Trigger document change + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME), + ) + .build() + val schema = session.setSchema(setSchemaRequest) + assertThat(schema.get()).isNotNull() + val appFunctionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, TEST_FUNCTION_ID, "") + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments(appFunctionRuntimeMetadata) + .build() + val putResult = session.put(putDocumentsRequest).get() + assertThat(putResult.isSuccess).isTrue() + } + assertThat( + settableDocumentChangeInfo + .get() + .changedDocumentIds + .contains( + AppFunctionRuntimeMetadata.getDocumentIdForAppFunction( + TEST_TARGET_PKG_NAME, + TEST_FUNCTION_ID, + ) + ) + ) + .isTrue() + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" + const val TEST_FUNCTION_ID: String = "print" + } +} diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt new file mode 100644 index 000000000000..1061da28f799 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -0,0 +1,296 @@ +/* + * 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.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME +import android.app.appsearch.AppSearchManager +import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SearchSpec +import android.app.appsearch.SetSchemaRequest +import android.util.ArrayMap +import android.util.ArraySet +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class MetadataSyncAdapterTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun getPackageToFunctionIdMap() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId") + } + + @Test + fun getPackageToFunctionIdMap_multipleDocuments() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val functionRuntimeMetadata1 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build() + val functionRuntimeMetadata2 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build() + val functionRuntimeMetadata3 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments( + functionRuntimeMetadata, + functionRuntimeMetadata1, + functionRuntimeMetadata2, + functionRuntimeMetadata3, + ) + .build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .setResultCountPerPage(1) + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]) + .containsExactly( + "testFunctionId", + "testFunctionId1", + "testFunctionId2", + "testFunctionId3", + ) + } + + @Test + fun getAddedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getAddedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2") + } + + @Test + fun getAddedFunctionsDiffMap_addedFunctionNewPackage() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getAddedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.size).isEqualTo(1) + assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getRemovedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" + } +} diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 5bb8ded6920e..52f1cbd89e13 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -107,6 +107,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; +import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; @@ -3053,6 +3054,74 @@ public class DisplayManagerServiceTest { } @Test + public void testBrightnessUpdates() { + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mShortMockedInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + final float invalidBrightness = -0.3f; + final float brightnessOff = -1.0f; + final float minimumBrightness = 0.0f; + final float validBrightness = 0.5f; + + Settings.System.putInt(mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); + + // set and check valid brightness + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, validBrightness); + waitForIdleHandler(mPowerHandler); + assertEquals(validBrightness, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + + // set and check invalid brightness + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, invalidBrightness); + waitForIdleHandler(mPowerHandler); + assertEquals(PowerManager.BRIGHTNESS_MIN, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + + // reset and check valid brightness + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, validBrightness); + waitForIdleHandler(mPowerHandler); + assertEquals(validBrightness, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + + // set and check brightness off + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, brightnessOff); + waitForIdleHandler(mPowerHandler); + assertEquals(PowerManager.BRIGHTNESS_MIN, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + + // reset and check valid brightness + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, validBrightness); + waitForIdleHandler(mPowerHandler); + assertEquals(validBrightness, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + + // set and check minimum brightness + waitForIdleHandler(mPowerHandler); + displayManagerBinderService.setBrightness(Display.DEFAULT_DISPLAY, minimumBrightness); + waitForIdleHandler(mPowerHandler); + assertEquals(PowerManager.BRIGHTNESS_MIN, + displayManagerBinderService.getBrightness(Display.DEFAULT_DISPLAY), + FLOAT_TOLERANCE); + } + + @Test public void testResolutionChangeGetsBackedUp() throws Exception { when(mMockFlags.isResolutionBackupRestoreEnabled()).thenReturn(true); DisplayManagerService displayManager = diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index f9dc12258667..da79f301ee3c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -226,7 +226,7 @@ public class BrightnessClamperControllerTest { float clampedBrightness = 0.6f; float customAnimationRate = 0.01f; when(mMockClamper.getBrightnessCap()).thenReturn(clampedBrightness); - when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.THERMAL); + when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.POWER); when(mMockClamper.getCustomAnimationRate()).thenReturn(customAnimationRate); when(mMockClamper.isActive()).thenReturn(false); mTestInjector.mCapturedChangeListener.onChanged(); @@ -250,7 +250,7 @@ public class BrightnessClamperControllerTest { float clampedBrightness = 0.6f; float customAnimationRate = 0.01f; when(mMockClamper.getBrightnessCap()).thenReturn(clampedBrightness); - when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.THERMAL); + when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.POWER); when(mMockClamper.getCustomAnimationRate()).thenReturn(customAnimationRate); when(mMockClamper.isActive()).thenReturn(true); mTestInjector.mCapturedChangeListener.onChanged(); @@ -274,7 +274,7 @@ public class BrightnessClamperControllerTest { float clampedBrightness = 0.8f; float customAnimationRate = 0.01f; when(mMockClamper.getBrightnessCap()).thenReturn(clampedBrightness); - when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.THERMAL); + when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.POWER); when(mMockClamper.getCustomAnimationRate()).thenReturn(customAnimationRate); when(mMockClamper.isActive()).thenReturn(true); mTestInjector.mCapturedChangeListener.onChanged(); @@ -298,7 +298,7 @@ public class BrightnessClamperControllerTest { float clampedBrightness = 0.6f; float customAnimationRate = 0.01f; when(mMockClamper.getBrightnessCap()).thenReturn(clampedBrightness); - when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.THERMAL); + when(mMockClamper.getType()).thenReturn(BrightnessClamper.Type.POWER); when(mMockClamper.getCustomAnimationRate()).thenReturn(customAnimationRate); when(mMockClamper.isActive()).thenReturn(true); mTestInjector.mCapturedChangeListener.onChanged(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalModifierTest.java index 9d16594fae93..35d384bccc7f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalClamperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessThermalModifierTest.java @@ -18,13 +18,16 @@ package com.android.server.display.brightness.clamper; import static com.android.server.display.config.DisplayDeviceConfigTestUtilsKt.createSensorData; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.hardware.display.BrightnessInfo; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManagerInternal; +import android.os.IBinder; import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.PowerManager; @@ -33,12 +36,14 @@ import android.os.Temperature; import android.provider.DeviceConfig; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.annotations.Keep; +import com.android.server.display.DisplayBrightnessState; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData; import com.android.server.display.DisplayDeviceConfig.ThermalBrightnessThrottlingData.ThrottlingLevel; +import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.clamper.BrightnessClamperController.ModifiersAggregatedState; import com.android.server.display.config.SensorData; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.testutils.FakeDeviceConfigInterface; @@ -54,10 +59,13 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; import java.util.List; +import java.util.Map; @RunWith(JUnitParamsRunner.class) -public class BrightnessThermalClamperTest { +public class BrightnessThermalModifierTest { + private static final int NO_MODIFIER = 0; private static final float FLOAT_TOLERANCE = 0.001f; @@ -66,28 +74,35 @@ public class BrightnessThermalClamperTest { private IThermalService mMockThermalService; @Mock private BrightnessClamperController.ClamperChangeListener mMockClamperChangeListener; + @Mock + private DisplayManagerInternal.DisplayPowerRequest mMockRequest; + @Mock + private DisplayDeviceConfig mMockDisplayDeviceConfig; + @Mock + private IBinder mMockBinder; private final FakeDeviceConfigInterface mFakeDeviceConfigInterface = new FakeDeviceConfigInterface(); private final TestHandler mTestHandler = new TestHandler(null); - private BrightnessThermalClamper mClamper; + private BrightnessThermalModifier mModifier; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mClamper = new BrightnessThermalClamper(new TestInjector(), mTestHandler, - mMockClamperChangeListener, new TestThermalData()); + when(mMockDisplayDeviceConfig.getTempSensor()) + .thenReturn(SensorData.loadTempSensorUnspecifiedConfig()); + mModifier = new BrightnessThermalModifier(new TestInjector(), mTestHandler, + mMockClamperChangeListener, + ClamperTestUtilsKt.createDisplayDeviceData(mMockDisplayDeviceConfig, mMockBinder)); mTestHandler.flush(); } - @Test - public void testTypeIsThermal() { - assertEquals(BrightnessClamper.Type.THERMAL, mClamper.getType()); - } @Test public void testNoThrottlingData() { - assertFalse(mClamper.isActive()); - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + 0.3f, true, + PowerManager.BRIGHTNESS_MAX, 0.3f, + false, true); } @Keep @@ -129,15 +144,19 @@ public class BrightnessThermalClamperTest { @Temperature.ThrottlingStatus int throttlingStatus, boolean expectedActive, float expectedBrightness) throws RemoteException { IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); - mClamper.onDisplayChanged(new TestThermalData(throttlingLevels)); + onDisplayChange(throttlingLevels); mTestHandler.flush(); - assertFalse(mClamper.isActive()); - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); thermalEventListener.notifyThrottling(createSkinTemperature(throttlingStatus)); mTestHandler.flush(); - assertEquals(expectedActive, mClamper.isActive()); - assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + expectedBrightness, expectedBrightness, + expectedActive, !expectedActive); } @Test @@ -148,13 +167,56 @@ public class BrightnessThermalClamperTest { IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); thermalEventListener.notifyThrottling(createSkinTemperature(throttlingStatus)); mTestHandler.flush(); - assertFalse(mClamper.isActive()); - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); - mClamper.onDisplayChanged(new TestThermalData(throttlingLevels)); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); + + onDisplayChange(throttlingLevels); + mTestHandler.flush(); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + expectedBrightness, expectedBrightness, + expectedActive, !expectedActive); + } + + @Test + public void testAppliesFastChangeOnlyOnActivation() throws RemoteException { + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); + onDisplayChange(List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 0.5f))); + mTestHandler.flush(); + + thermalEventListener.notifyThrottling(createSkinTemperature(Temperature.THROTTLING_SEVERE)); + mTestHandler.flush(); + + // expectedSlowChange = false + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + 0.5f, 0.5f, + true, false); + + // slowChange is unchanged + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + 0.5f, 0.5f, + true, true); + } + + @Test + public void testCapsMaxBrightnessOnly_currentBrightnessIsLowAndFastChange() + throws RemoteException { + IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); + onDisplayChange(List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 0.5f))); mTestHandler.flush(); - assertEquals(expectedActive, mClamper.isActive()); - assertEquals(expectedBrightness, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + thermalEventListener.notifyThrottling(createSkinTemperature(Temperature.THROTTLING_SEVERE)); + mTestHandler.flush(); + + assertModifierState( + 0.1f, false, + 0.5f, 0.1f, + true, false); } @Test @@ -162,28 +224,37 @@ public class BrightnessThermalClamperTest { IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); thermalEventListener.notifyThrottling(createSkinTemperature(Temperature.THROTTLING_SEVERE)); mTestHandler.flush(); - assertFalse(mClamper.isActive()); - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); - mClamper.onDisplayChanged(new TestThermalData( - List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 0.5f)))); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); + + onDisplayChange(List.of(new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE, 0.5f))); mTestHandler.flush(); - assertTrue(mClamper.isActive()); - assertEquals(0.5f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + 0.5f, 0.5f, + true, false); overrideThrottlingData("displayId,1,emergency,0.4"); - mClamper.onDeviceConfigChanged(); + mModifier.onDeviceConfigChanged(); mTestHandler.flush(); - assertFalse(mClamper.isActive()); - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); overrideThrottlingData("displayId,1,moderate,0.4"); - mClamper.onDeviceConfigChanged(); + mModifier.onDeviceConfigChanged(); mTestHandler.flush(); - assertTrue(mClamper.isActive()); - assertEquals(0.4f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + 0.4f, 0.4f, + true, false); } @Test @@ -191,35 +262,41 @@ public class BrightnessThermalClamperTest { final int severity = PowerManager.THERMAL_STATUS_SEVERE; IThermalEventListener thermalEventListener = captureSkinThermalEventListener(); // Update config to listen to display type sensor. - final SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); - final TestThermalData thermalData = - new TestThermalData( - DISPLAY_ID, - DisplayDeviceConfig.DEFAULT_ID, - List.of(new ThrottlingLevel(severity, 0.5f)), - tempSensor); - mClamper.onDisplayChanged(thermalData); + SensorData tempSensor = createSensorData("DISPLAY", "VIRTUAL-SKIN-DISPLAY"); + + when(mMockDisplayDeviceConfig.getTempSensor()).thenReturn(tempSensor); + onDisplayChange(List.of(new ThrottlingLevel(severity, 0.5f))); mTestHandler.flush(); + verify(mMockThermalService).unregisterThermalEventListener(thermalEventListener); thermalEventListener = captureThermalEventListener(Temperature.TYPE_DISPLAY); - assertFalse(mClamper.isActive()); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); // Verify no throttling triggered when any other sensor notification received. thermalEventListener.notifyThrottling(createSkinTemperature(severity)); mTestHandler.flush(); - assertFalse(mClamper.isActive()); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); thermalEventListener.notifyThrottling(createDisplayTemperature("OTHER-SENSOR", severity)); mTestHandler.flush(); - assertFalse(mClamper.isActive()); - - assertEquals(PowerManager.BRIGHTNESS_MAX, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + PowerManager.BRIGHTNESS_MAX, PowerManager.BRIGHTNESS_MAX, + false, true); // Verify throttling triggered when display sensor of given name throttled. thermalEventListener.notifyThrottling(createDisplayTemperature(tempSensor.name, severity)); mTestHandler.flush(); - assertTrue(mClamper.isActive()); - assertEquals(0.5f, mClamper.getBrightnessCap(), FLOAT_TOLERANCE); + assertModifierState( + PowerManager.BRIGHTNESS_MAX, true, + 0.5f, 0.5f, + true, false); } private IThermalEventListener captureSkinThermalEventListener() throws RemoteException { @@ -248,65 +325,54 @@ public class BrightnessThermalClamperTest { DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA, data); } - private class TestInjector extends BrightnessThermalClamper.Injector { - @Override - IThermalService getThermalService() { - return mMockThermalService; - } - - @Override - DeviceConfigParameterProvider getDeviceConfigParameterProvider() { - return new DeviceConfigParameterProvider(mFakeDeviceConfigInterface); - } + private void onDisplayChange(List<ThrottlingLevel> throttlingLevels) { + Map<String, ThermalBrightnessThrottlingData> throttlingLevelsMap = new HashMap<>(); + throttlingLevelsMap.put(DisplayDeviceConfig.DEFAULT_ID, + ThermalBrightnessThrottlingData.create(throttlingLevels)); + when(mMockDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId()) + .thenReturn(throttlingLevelsMap); + mModifier.onDisplayChanged(ClamperTestUtilsKt.createDisplayDeviceData( + mMockDisplayDeviceConfig, mMockBinder, DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID)); } - private static class TestThermalData implements BrightnessThermalClamper.ThermalData { - - private final String mUniqueDisplayId; - private final String mDataId; - private final ThermalBrightnessThrottlingData mData; - private final SensorData mTempSensor; - - private TestThermalData() { - this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, null, - SensorData.loadTempSensorUnspecifiedConfig()); - } - - private TestThermalData(List<ThrottlingLevel> data) { - this(DISPLAY_ID, DisplayDeviceConfig.DEFAULT_ID, data, - SensorData.loadTempSensorUnspecifiedConfig()); - } - - private TestThermalData(String uniqueDisplayId, String dataId, List<ThrottlingLevel> data, - SensorData tempSensor) { - mUniqueDisplayId = uniqueDisplayId; - mDataId = dataId; - mData = ThermalBrightnessThrottlingData.create(data); - mTempSensor = tempSensor; - } - - @NonNull - @Override - public String getUniqueDisplayId() { - return mUniqueDisplayId; - } + private void assertModifierState( + float currentBrightness, + boolean currentSlowChange, + float maxBrightness, float brightness, + boolean isActive, + boolean isSlowChange) { + ModifiersAggregatedState modifierState = new ModifiersAggregatedState(); + DisplayBrightnessState.Builder stateBuilder = DisplayBrightnessState.builder(); + stateBuilder.setBrightness(currentBrightness); + stateBuilder.setIsSlowChange(currentSlowChange); + + int maxBrightnessReason = isActive ? BrightnessInfo.BRIGHTNESS_MAX_REASON_THERMAL + : BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; + int modifier = isActive ? BrightnessReason.MODIFIER_THROTTLED : NO_MODIFIER; + + mModifier.applyStateChange(modifierState); + assertThat(modifierState.mMaxBrightness).isEqualTo(maxBrightness); + assertThat(modifierState.mMaxBrightnessReason).isEqualTo(maxBrightnessReason); + + mModifier.apply(mMockRequest, stateBuilder); + + assertThat(stateBuilder.getMaxBrightness()).isWithin(FLOAT_TOLERANCE).of(maxBrightness); + assertThat(stateBuilder.getBrightness()).isWithin(FLOAT_TOLERANCE).of(brightness); + assertThat(stateBuilder.getBrightnessMaxReason()).isEqualTo(maxBrightnessReason); + assertThat(stateBuilder.getBrightnessReason().getModifier()).isEqualTo(modifier); + assertThat(stateBuilder.isSlowChange()).isEqualTo(isSlowChange); + } - @NonNull - @Override - public String getThermalThrottlingDataId() { - return mDataId; - } - @Nullable + private class TestInjector extends BrightnessThermalModifier.Injector { @Override - public ThermalBrightnessThrottlingData getThermalBrightnessThrottlingData() { - return mData; + IThermalService getThermalService() { + return mMockThermalService; } - @NonNull @Override - public SensorData getTempSensor() { - return mTempSensor; + DeviceConfigParameterProvider getDeviceConfigParameterProvider() { + return new DeviceConfigParameterProvider(mFakeDeviceConfigInterface); } } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt index 5fd848f6adcc..f21749e0b843 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/ClamperTestUtils.kt @@ -21,6 +21,7 @@ import android.view.Display import com.android.server.display.DisplayDeviceConfig import com.android.server.display.brightness.clamper.BrightnessClamperController.DisplayDeviceData +@JvmOverloads fun createDisplayDeviceData( displayDeviceConfig: DisplayDeviceConfig, displayToken: IBinder, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java new file mode 100644 index 000000000000..c8e4f89aaee6 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 The Android Open 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.job; + +import static android.app.job.Flags.FLAG_CLEANUP_EMPTY_JOBS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; + +import android.app.job.IJobCallback; +import android.app.job.JobParameters; +import android.net.Uri; +import android.os.Parcel; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public class JobParametersTest { + private static final String TAG = JobParametersTest.class.getSimpleName(); + private static final int TEST_JOB_ID_1 = 123; + private static final String TEST_NAMESPACE = "TEST_NAMESPACE"; + private static final String TEST_DEBUG_STOP_REASON = "TEST_DEBUG_STOP_REASON"; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private MockitoSession mMockingSession; + @Mock private Parcel mMockParcel; + @Mock private IJobCallback.Stub mMockJobCallbackStub; + + @Before + public void setUp() throws Exception { + mMockingSession = + mockitoSession().initMocks(this).strictness(Strictness.LENIENT).startMocking(); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + + when(mMockParcel.readInt()) + .thenReturn(TEST_JOB_ID_1) // Job ID + .thenReturn(0) // No clip data + .thenReturn(0) // No deadline expired + .thenReturn(0) // No network + .thenReturn(0) // No stop reason + .thenReturn(0); // Internal stop reason + when(mMockParcel.readString()) + .thenReturn(TEST_NAMESPACE) // Job namespace + .thenReturn(TEST_DEBUG_STOP_REASON); // Debug stop reason + when(mMockParcel.readPersistableBundle()).thenReturn(null); + when(mMockParcel.readBundle()).thenReturn(null); + when(mMockParcel.readStrongBinder()).thenReturn(mMockJobCallbackStub); + when(mMockParcel.readBoolean()) + .thenReturn(false) // expedited + .thenReturn(false); // user initiated + when(mMockParcel.createTypedArray(any())).thenReturn(new Uri[0]); + when(mMockParcel.createStringArray()).thenReturn(new String[0]); + } + + /** + * Test to verify that the JobParameters created using Non-Parcelable constructor has not + * cleaner attached + */ + @Test + public void testJobParametersNonParcelableConstructor_noCleaner() { + JobParameters jobParameters = + new JobParameters( + null, + TEST_NAMESPACE, + TEST_JOB_ID_1, + null, + null, + null, + 0, + false, + false, + false, + null, + null, + null); + + // Verify that cleaner is not registered + assertThat(jobParameters.getCleanable()).isNull(); + assertThat(jobParameters.getJobCleanupCallback()).isNull(); + } + + /** + * Test to verify that the JobParameters created using Parcelable constructor has not cleaner + * attached + */ + @Test + public void testJobParametersParcelableConstructor_noCleaner() { + JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel); + + // Verify that cleaner is not registered + assertThat(jobParameters.getCleanable()).isNull(); + assertThat(jobParameters.getJobCleanupCallback()).isNull(); + } + + /** Test to verify that the JobParameters Cleaner is disabled */ + @RequiresFlagsEnabled(FLAG_CLEANUP_EMPTY_JOBS) + @Test + public void testCleanerWithLeakedJobCleanerDisabled_flagCleanupEmptyJobsEnabled() { + // Inject real JobCallbackCleanup + JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel); + + // Enable the cleaner + jobParameters.enableCleaner(); + + // Verify the cleaner is enabled + assertThat(jobParameters.getCleanable()).isNotNull(); + assertThat(jobParameters.getJobCleanupCallback()).isNotNull(); + assertThat(jobParameters.getJobCleanupCallback().isCleanerEnabled()).isTrue(); + + // Disable the cleaner + jobParameters.disableCleaner(); + + // Verify the cleaner is disabled + assertThat(jobParameters.getCleanable()).isNull(); + assertThat(jobParameters.getJobCleanupCallback()).isNull(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 2e6c93cb92aa..566feb7e3d80 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -35,6 +35,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; +import static com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity.EXTRA_TYPE_TO_CHOOSE; import static com.android.server.accessibility.AccessibilityManagerService.ACTION_LAUNCH_HEARING_DEVICES_DIALOG; import static com.android.window.flags.Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER; @@ -2046,6 +2047,53 @@ public class AccessibilityManagerServiceTest { } @Test + public void showAccessibilityTargetSelection_navBarNavigationMode_softwareExtra() { + mFakePermissionEnforcer.grant(Manifest.permission.STATUS_BAR_SERVICE); + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + NAVIGATION_MODE, NAV_BAR_MODE_3BUTTON, userState.mUserId); + + mA11yms.notifyAccessibilityButtonLongClicked(Display.DEFAULT_DISPLAY); + mTestableLooper.processAllMessages(); + + assertStartActivityWithExpectedShortcutType(mTestableContext.getMockContext(), SOFTWARE); + } + + @Test + @DisableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void showAccessibilityTargetSelection_gestureNavigationMode_softwareExtra() { + mFakePermissionEnforcer.grant(Manifest.permission.STATUS_BAR_SERVICE); + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + NAVIGATION_MODE, NAV_BAR_MODE_GESTURAL, userState.mUserId); + + mA11yms.notifyAccessibilityButtonLongClicked(Display.DEFAULT_DISPLAY); + mTestableLooper.processAllMessages(); + + assertStartActivityWithExpectedShortcutType(mTestableContext.getMockContext(), SOFTWARE); + } + + @Test + @EnableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void showAccessibilityTargetSelection_gestureNavigationMode_gestureExtra() { + mFakePermissionEnforcer.grant(Manifest.permission.STATUS_BAR_SERVICE); + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + NAVIGATION_MODE, NAV_BAR_MODE_GESTURAL, userState.mUserId); + + mA11yms.notifyAccessibilityButtonLongClicked(Display.DEFAULT_DISPLAY); + mTestableLooper.processAllMessages(); + + assertStartActivityWithExpectedShortcutType(mTestableContext.getMockContext(), GESTURE); + } + + @Test public void registerUserInitializationCompleteCallback_isRegistered() { mA11yms.mUserInitializationCompleteCallbacks.clear(); @@ -2075,6 +2123,43 @@ public class AccessibilityManagerServiceTest { UserHandle.MIN_SECONDARY_USER_ID); } + @Test + @DisableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void getShortcutTypeForGenericShortcutCalls_softwareType() { + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + + assertThat(mA11yms.getShortcutTypeForGenericShortcutCalls(userState.mUserId)) + .isEqualTo(SOFTWARE); + } + + @Test + @EnableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void getShortcutTypeForGenericShortcutCalls_gestureNavigationMode_gestureType() { + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + NAVIGATION_MODE, NAV_BAR_MODE_GESTURAL, userState.mUserId); + + assertThat(mA11yms.getShortcutTypeForGenericShortcutCalls(userState.mUserId)) + .isEqualTo(GESTURE); + } + + @Test + @EnableFlags(android.provider.Flags.FLAG_A11Y_STANDALONE_GESTURE_ENABLED) + public void getShortcutTypeForGenericShortcutCalls_buttonNavigationMode_softwareType() { + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + Settings.Secure.putIntForUser(mTestableContext.getContentResolver(), + NAVIGATION_MODE, NAV_BAR_MODE_3BUTTON, userState.mUserId); + + assertThat(mA11yms.getShortcutTypeForGenericShortcutCalls(userState.mUserId)) + .isEqualTo(SOFTWARE); + } + private Set<String> readStringsFromSetting(String setting) { final Set<String> result = new ArraySet<>(); mA11yms.readColonDelimitedSettingToSet( @@ -2148,6 +2233,14 @@ public class AccessibilityManagerServiceTest { Intent.EXTRA_COMPONENT_NAME)).isEqualTo(componentName); } + private void assertStartActivityWithExpectedShortcutType(Context mockContext, + @UserShortcutType int shortcutType) { + verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(), + any(Bundle.class), any(UserHandle.class)); + assertThat(mIntentArgumentCaptor.getValue().getIntExtra( + EXTRA_TYPE_TO_CHOOSE, -1)).isEqualTo(shortcutType); + } + private void setupShortcutTargetServices() { setupShortcutTargetServices(mA11yms.getCurrentUserState()); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index 62fa95149067..627b5e39a20a 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -28,10 +28,13 @@ import static android.view.accessibility.AccessibilityManager.STATE_FLAG_ACCESSI import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED; import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP; import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain; import static com.google.common.truth.Truth.assertThat; @@ -67,6 +70,8 @@ import androidx.test.InstrumentationRegistry; import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.After; @@ -504,6 +509,36 @@ public class AccessibilityUserStateTest { assertThat(actual).containsExactly(tileComponent, mMockServiceInfo); } + @Test + public void isShortcutMagnificationEnabledLocked_anyShortcutType_returnsTrue() { + // Clear every shortcut + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + setMagnificationForShortcutType(shortcutType, false); + } + // Check each shortcut individually + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + // Setup + setMagnificationForShortcutType(shortcutType, true); + + // Checking + assertThat(mUserState.getShortcutTargetsLocked(shortcutType)) + .containsExactly(MAGNIFICATION_CONTROLLER_NAME); + assertThat(mUserState.isShortcutMagnificationEnabledLocked()).isTrue(); + + // Cleanup + setMagnificationForShortcutType(shortcutType, false); + } + } + + @Test + public void isShortcutMagnificationEnabledLocked_noShortcutTypes_returnsFalse() { + // Clear every shortcut + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + setMagnificationForShortcutType(shortcutType, false); + } + assertThat(mUserState.isShortcutMagnificationEnabledLocked()).isFalse(); + } + private int getSecureIntForUser(String key, int userId) { return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId); } @@ -511,4 +546,16 @@ public class AccessibilityUserStateTest { private void putSecureIntForUser(String key, int value, int userId) { Settings.Secure.putIntForUser(mMockResolver, key, value, userId); } + + private void setMagnificationForShortcutType( + @UserShortcutType int shortcutType, boolean enabled) { + if (shortcutType == TRIPLETAP) { + mUserState.setMagnificationSingleFingerTripleTapEnabledLocked(enabled); + } else if (shortcutType == TWOFINGER_DOUBLETAP) { + mUserState.setMagnificationTwoFingerTripleTapEnabledLocked(enabled); + } else { + mUserState.updateShortcutTargetsLocked( + enabled ? Set.of(MAGNIFICATION_CONTROLLER_NAME) : Set.of(), shortcutType); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 6b8e414255cd..b4b36125f770 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -558,7 +558,9 @@ public class BiometricServiceTest { waitForIdle(); verify(mReceiver1).onError( eq(BiometricAuthenticator.TYPE_NONE), - eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), + eq(Flags.mandatoryBiometrics() + ? BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS + : BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(0 /* vendorCode */)); // Enrolled, not disabled in settings, user requires confirmation in settings @@ -1450,7 +1452,9 @@ public class BiometricServiceTest { } @Test - public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCanAuthenticate_whenBiometricsNotEnabledForApps_returnsHardwareUnavailable() + throws Exception { setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) @@ -1468,6 +1472,25 @@ public class BiometricServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception { + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) + .thenReturn(true); + + // When only biometric is requested + int authenticators = Authenticators.BIOMETRIC_STRONG; + assertEquals(BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS, + invokeCanAuthenticate(mBiometricService, authenticators)); + + // When credential and biometric are requested + authenticators = Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL; + assertEquals(BiometricManager.BIOMETRIC_SUCCESS, + invokeCanAuthenticate(mBiometricService, authenticators)); + } + + @Test public void testCanAuthenticate_whenNoBiometricSensor() throws Exception { mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider); mBiometricService.onStart(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java index 760d38e855a6..b758f57ff407 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NO import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE; @@ -266,6 +267,29 @@ public class PreAuthInfoTest { @Test @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCalculateByPriority() + throws Exception { + when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false); + when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); + + BiometricSensor faceSensor = getFaceSensor(); + BiometricSensor fingerprintSensor = getFingerprintSensor(); + PromptInfo promptInfo = new PromptInfo(); + promptInfo.setConfirmationRequested(false /* requireConfirmation */); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); + PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(faceSensor, fingerprintSensor), + 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + + assertThat(preAuthInfo.eligibleSensors).hasSize(0); + assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo( + BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) public void testMandatoryBiometricsNegativeButtonText_whenSet() throws Exception { when(mTrustManager.isInSignificantPlace()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java index 6ace9f14757c..fca0cfbc7d2f 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java @@ -20,6 +20,7 @@ import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM; import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_BOOT_UP; +import static com.android.server.hdmi.HdmiCecFeatureAction.DELAY_GIVE_AUDIO_STATUS; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; @@ -582,6 +583,9 @@ public abstract class BaseAbsoluteVolumeBehaviorTest { ); mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(DELAY_GIVE_AUDIO_STATUS); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains( HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(), getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP)); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java index 673140390ae7..ec44a918f8e8 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseTvToAudioSystemAvbTest.java @@ -17,6 +17,7 @@ package com.android.server.hdmi; import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP; +import static com.android.server.hdmi.HdmiCecFeatureAction.DELAY_GIVE_AUDIO_STATUS; import static com.google.common.truth.Truth.assertThat; @@ -223,6 +224,9 @@ public abstract class BaseTvToAudioSystemAvbTest extends BaseAbsoluteVolumeBehav ); mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(DELAY_GIVE_AUDIO_STATUS); + mTestLooper.dispatchAll(); + assertThat(mNativeWrapper.getResultMessages()).contains( HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(), getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP)); diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index a5f1fcd01aa1..2d957401e6bd 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -30,6 +30,7 @@ import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF; import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON; import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS; import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS; +import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX; import static com.google.common.truth.Truth.assertThat; @@ -273,13 +274,12 @@ public class HdmiCecLocalDeviceTvTest { assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated); // Finish querying SADs - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); mNativeWrapper.clearResultMessages(); @@ -685,15 +685,43 @@ public class HdmiCecLocalDeviceTvTest { assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated); // Finish querying SADs - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } + + assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); + } + + @Test + public void handleInitiateArc_arcAlreadyEstablished_noRequestSad() { + // Emulate Audio device on port 0x2000 (supports ARC) + mNativeWrapper.setPortConnectionStatus(2, true); + HdmiCecMessage reportPhysicalAddress = + HdmiCecMessageBuilder.buildReportPhysicalAddressCommand( + ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM); + mNativeWrapper.onCecMessage(reportPhysicalAddress); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + + assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isFalse(); + + HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc( + ADDR_AUDIO_SYSTEM, + ADDR_TV); + mNativeWrapper.onCecMessage(requestArcInitiation); mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); + // Finish querying SADs + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } + + assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isTrue(); } @Test @@ -970,13 +998,12 @@ public class HdmiCecLocalDeviceTvTest { // <Report ARC Initiated> should only be sent after SAD querying is done assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated); // Finish querying SADs - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); mNativeWrapper.clearResultMessages(); @@ -1171,13 +1198,12 @@ public class HdmiCecLocalDeviceTvTest { mTestLooper.dispatchAll(); // Finish querying SADs - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } // ARC should be established after RequestSadAction is finished assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); @@ -1327,13 +1353,12 @@ public class HdmiCecLocalDeviceTvTest { assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated); // Finish querying SADs - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); - mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); - mTestLooper.dispatchAll(); + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + } assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated); } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java index f8e465c4c36f..4cf293758519 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java @@ -18,6 +18,7 @@ package com.android.server.hdmi; import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY; import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM; +import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX; import static com.google.common.truth.Truth.assertThat; @@ -144,7 +145,7 @@ public class RequestSadActionTest { } @Test - public void noResponse_queryAgainOnce_emptyResult() { + public void noResponse_queryAgain_emptyResult() { RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM, mCallback); action.start(); @@ -154,13 +155,13 @@ public class RequestSadActionTest { HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor( mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM, CODECS_TO_QUERY_1.stream().mapToInt(i -> i).toArray()); - assertThat(mNativeWrapper.getResultMessages()).contains(expected1); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected1); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); + + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(expected1); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + } assertThat(mSupportedSads).isNotNull(); assertThat(mSupportedSads.size()).isEqualTo(0); @@ -507,7 +508,7 @@ public class RequestSadActionTest { } @Test - public void invalidMessageLength_queryAgainOnce() { + public void invalidMessageLength_queryAgain() { RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM, mCallback); action.start(); @@ -524,16 +525,13 @@ public class RequestSadActionTest { 0x27, 0x20, 0x0A}; HdmiCecMessage response1 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor( Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_1); - assertThat(mNativeWrapper.getResultMessages()).contains(expected1); - mNativeWrapper.clearResultMessages(); - action.processCommand(response1); - mTestLooper.dispatchAll(); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); - assertThat(mNativeWrapper.getResultMessages()).contains(expected1); - mNativeWrapper.clearResultMessages(); - mTestLooper.moveTimeForward(TIMEOUT_MS); - mTestLooper.dispatchAll(); + + for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) { + assertThat(mNativeWrapper.getResultMessages()).contains(expected1); + mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + } assertThat(mSupportedSads).isNotNull(); assertThat(mSupportedSads.size()).isEqualTo(0); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 4a199738cccd..1890879da69d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.app.KeyguardManager; import android.app.UiModeManager; import android.app.WallpaperManager; import android.content.BroadcastReceiver; @@ -78,6 +79,7 @@ public class DefaultDeviceEffectsApplierTest { private DefaultDeviceEffectsApplier mApplier; @Mock PowerManager mPowerManager; @Mock ColorDisplayManager mColorDisplayManager; + @Mock KeyguardManager mKeyguardManager; @Mock UiModeManager mUiModeManager; @Mock WallpaperManager mWallpaperManager; @@ -87,6 +89,7 @@ public class DefaultDeviceEffectsApplierTest { mContext = spy(new TestableContext(InstrumentationRegistry.getContext(), null)); mContext.addMockSystemService(PowerManager.class, mPowerManager); mContext.addMockSystemService(ColorDisplayManager.class, mColorDisplayManager); + mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager); mContext.addMockSystemService(UiModeManager.class, mUiModeManager); mContext.addMockSystemService(WallpaperManager.class, mWallpaperManager); when(mWallpaperManager.isWallpaperSupported()).thenReturn(true); @@ -311,6 +314,22 @@ public class DefaultDeviceEffectsApplierTest { } @Test + @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI}) + public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately( + @TestParameter ZenChangeOrigin origin) { + + when(mPowerManager.isInteractive()).thenReturn(true); + when(mKeyguardManager.isKeyguardLocked()).thenReturn(true); + + mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(), + origin.value()); + + // Effect was applied, and no broadcast receiver was registered. + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); + verify(mContext, never()).registerReceiver(any(), any(), anyInt()); + } + + @Test @TestParameters({"{origin: ORIGIN_USER_IN_SYSTEMUI}", "{origin: ORIGIN_USER_IN_APP}", "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"}) public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin( diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index e694c0b4afc1..536dcfb3579c 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -42,7 +42,6 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; import android.app.ActivityManager; import android.app.AppOpsManager; @@ -135,15 +134,13 @@ public class PhoneWindowManagerTests { doNothing().when(mPhoneWindowManager).initializeHdmiState(); final boolean[] isScreenTurnedOff = { false }; final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); - doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(); + doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff( + anyBoolean()); doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly(); doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully(); mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); - final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer = - mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class); - doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString()); final PowerManager pm = mock(PowerManager.class); doReturn(true).when(pm).isInteractive(); doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); @@ -155,9 +152,8 @@ public class PhoneWindowManagerTests { assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); // Skip sleep-token for non-sleep-screen-off. - clearInvocations(tokenAcquirer); mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); - verify(tokenAcquirer, never()).acquire(anyInt()); + verify(displayPolicy).screenTurnedOff(false /* acquireSleepToken */); assertThat(isScreenTurnedOff[0]).isTrue(); // Apply sleep-token for sleep-screen-off. @@ -165,21 +161,10 @@ public class PhoneWindowManagerTests { mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue(); mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); - verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY)); + verify(displayPolicy).screenTurnedOff(true /* acquireSleepToken */); mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); - - // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep - // token can still be acquired. - isScreenTurnedOff[0] = false; - clearInvocations(tokenAcquirer); - mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); - verify(tokenAcquirer, never()).acquire(anyInt()); - assertThat(displayPolicy.isScreenOnEarly()).isFalse(); - assertThat(displayPolicy.isScreenOnFully()).isFalse(); - mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); - verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 1e035dab3c5e..e2e76d6ef4e5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3231,7 +3231,7 @@ public class ActivityRecordTests extends WindowTestsBase { mDisplayContent.mOpeningApps.remove(activity); mDisplayContent.mClosingApps.remove(activity); activity.commitVisibility(false /* visible */, false /* performLayout */); - mDisplayContent.getDisplayPolicy().screenTurnedOff(); + mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */); final KeyguardController controller = mSupervisor.getKeyguardController(); doReturn(true).when(controller).isKeyguardGoingAway(anyInt()); activity.setVisibility(true); 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 caeb41c78967..f32a234f3e40 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -284,11 +284,11 @@ public class DisplayPolicyTests extends WindowTestsBase { final DisplayPolicy policy = mDisplayContent.getDisplayPolicy(); policy.addWindowLw(mNotificationShadeWindow, mNotificationShadeWindow.mAttrs); - policy.screenTurnedOff(); + policy.screenTurnedOff(false /* acquireSleepToken */); policy.setAwake(false); policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); - policy.screenTurnedOff(); + policy.screenTurnedOff(false /* acquireSleepToken */); assertFalse(wpc.isShowingUiWhileDozing()); policy.screenTurningOn(null /* screenOnListener */); @@ -393,7 +393,7 @@ public class DisplayPolicyTests extends WindowTestsBase { info.logicalWidth, info.logicalHeight).mConfigFrame); // If screen is not fully turned on, then the cache should be preserved. - displayPolicy.screenTurnedOff(); + displayPolicy.screenTurnedOff(false /* acquireSleepToken */); final TransitionController transitionController = mDisplayContent.mTransitionController; spyOn(transitionController); doReturn(true).when(transitionController).isCollecting(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 2e488d8127bb..09fe75da18cb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -22,6 +22,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.DisplayCutout.NO_CUTOUT; import static android.view.IWindowManager.FIXED_TO_USER_ROTATION_DEFAULT; import static android.view.IWindowManager.FIXED_TO_USER_ROTATION_DISABLED; @@ -78,7 +79,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.util.test.FakeSettingsProvider; -import com.android.server.LocalServices; import com.android.server.UiThread; import com.android.server.policy.WindowManagerPolicy; import com.android.server.statusbar.StatusBarManagerInternal; @@ -86,7 +86,6 @@ import com.android.server.testutils.OffsettableClock; import com.android.server.testutils.TestHandler; import com.android.server.wm.DisplayContent.FixedRotationTransitionListener; -import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -168,25 +167,10 @@ public class DisplayRotationTests { public void setUp() { FakeSettingsProvider.clearSettingsProvider(); - mPreviousStatusBarManagerInternal = LocalServices.getService( - StatusBarManagerInternal.class); - LocalServices.removeServiceForTest(StatusBarManagerInternal.class); - mMockStatusBarManagerInternal = mock(StatusBarManagerInternal.class); - LocalServices.addService(StatusBarManagerInternal.class, mMockStatusBarManagerInternal); mDisplayRotationImmersiveAppCompatPolicyMock = null; mBuilder = new DisplayRotationBuilder(); } - @After - public void tearDown() { - LocalServices.removeServiceForTest(StatusBarManagerInternal.class); - if (mPreviousStatusBarManagerInternal != null) { - LocalServices.addService(StatusBarManagerInternal.class, - mPreviousStatusBarManagerInternal); - mPreviousStatusBarManagerInternal = null; - } - } - // ================================ // Display Settings Related Tests // ================================ @@ -677,7 +661,8 @@ public class DisplayRotationTests { mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_90)); assertTrue(waitForUiHandler()); - verify(mMockStatusBarManagerInternal).onProposedRotationChanged(Surface.ROTATION_90, true); + verify(mMockStatusBarManagerInternal).onProposedRotationChanged(DEFAULT_DISPLAY, + Surface.ROTATION_90, true); } @Test @@ -697,7 +682,8 @@ public class DisplayRotationTests { mOrientationSensorListener.onSensorChanged(createSensorEvent(Surface.ROTATION_90)); assertTrue(waitForUiHandler()); - verify(mMockStatusBarManagerInternal).onProposedRotationChanged(Surface.ROTATION_90, true); + verify(mMockStatusBarManagerInternal).onProposedRotationChanged(DEFAULT_DISPLAY, + Surface.ROTATION_90, true); // An imaginary ActivityRecord.setRequestedOrientation call disables immersive mode: when(mDisplayRotationImmersiveAppCompatPolicyMock.isRotationLockEnforced( @@ -728,7 +714,7 @@ public class DisplayRotationTests { assertTrue(waitForUiHandler()); verify(mMockStatusBarManagerInternal) - .onProposedRotationChanged(Surface.ROTATION_180, true); + .onProposedRotationChanged(DEFAULT_DISPLAY, Surface.ROTATION_180, true); } @Test @@ -746,7 +732,7 @@ public class DisplayRotationTests { assertTrue(waitForUiHandler()); verify(mMockStatusBarManagerInternal) - .onProposedRotationChanged(Surface.ROTATION_180, false); + .onProposedRotationChanged(DEFAULT_DISPLAY, Surface.ROTATION_180, false); } @Test @@ -773,7 +759,7 @@ public class DisplayRotationTests { assertTrue(waitForUiHandler()); verify(mMockStatusBarManagerInternal) - .onProposedRotationChanged(Surface.ROTATION_180, false); + .onProposedRotationChanged(DEFAULT_DISPLAY, Surface.ROTATION_180, false); } @Test @@ -1526,6 +1512,7 @@ public class DisplayRotationTests { mMockDisplayContent = mock(DisplayContent.class); when(mMockDisplayContent.getDisplay()).thenReturn(mock(Display.class)); + when(mMockDisplayContent.getDisplayId()).thenReturn(DEFAULT_DISPLAY); mMockDisplayContent.isDefaultDisplay = mIsDefaultDisplay; when(mMockDisplayContent.calculateDisplayCutoutForRotation(anyInt())) .thenReturn(NO_CUTOUT); @@ -1541,6 +1528,9 @@ public class DisplayRotationTests { field.set(mMockDisplayContent, mock(FixedRotationTransitionListener.class)); mMockDisplayPolicy = mock(DisplayPolicy.class); + mMockStatusBarManagerInternal = mock(StatusBarManagerInternal.class); + when(mMockDisplayPolicy.getStatusBarManagerInternal()).thenReturn( + mMockStatusBarManagerInternal); mMockRes = mock(Resources.class); when(mMockContext.getResources()).thenReturn((mMockRes)); diff --git a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java index 1d14dc31fa26..bef4531c9f28 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java @@ -219,7 +219,7 @@ public class LockTaskControllerTest { mLockTaskController.startLockTaskMode(tr, false, TEST_UID); // THEN a pinning request should be shown - verify(mStatusBarManagerInternal).showScreenPinningRequest(anyInt()); + verify(mStatusBarManagerInternal).showScreenPinningRequest(anyInt(), anyInt()); } @Test @@ -769,9 +769,11 @@ public class LockTaskControllerTest { private Task getTask(Intent intent, int lockTaskAuth) { Task tr = mock(Task.class); + DisplayContent dc = mock(DisplayContent.class); tr.mLockTaskAuth = lockTaskAuth; tr.intent = intent; tr.mUserId = TEST_USER_ID; + tr.mDisplayContent = dc; return tr; } 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 cc1805aa933c..fd959b950e16 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -242,7 +242,7 @@ public class TaskFragmentTest extends WindowTestsBase { final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds()); final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy(); - displayPolicy.screenTurnedOff(); + displayPolicy.screenTurnedOff(false /* acquireSleepToken */); assertFalse(mTaskFragment.okToAnimate()); diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index 112471b2af57..c85374e0b660 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -376,7 +376,8 @@ interface ITelecomService { */ void requestLogMark(in String message); - void setTestPhoneAcctSuggestionComponent(String flattenedComponentName); + void setTestPhoneAcctSuggestionComponent(String flattenedComponentName, + in UserHandle userHandle); void setTestDefaultCallScreeningApp(String packageName); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index f0850af5fc2e..51e0c33ff705 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1456,21 +1456,21 @@ public class SubscriptionManager { public static final int SERVICE_CAPABILITY_MAX = SERVICE_CAPABILITY_DATA; /** - * Bitmask for {@code SERVICE_CAPABILITY_VOICE}. + * Bitmask for {@link #SERVICE_CAPABILITY_VOICE}. * @hide */ public static final int SERVICE_CAPABILITY_VOICE_BITMASK = serviceCapabilityToBitmask(SERVICE_CAPABILITY_VOICE); /** - * Bitmask for {@code SERVICE_CAPABILITY_SMS}. + * Bitmask for {@link #SERVICE_CAPABILITY_SMS}. * @hide */ public static final int SERVICE_CAPABILITY_SMS_BITMASK = serviceCapabilityToBitmask(SERVICE_CAPABILITY_SMS); /** - * Bitmask for {@code SERVICE_CAPABILITY_DATA}. + * Bitmask for {@link #SERVICE_CAPABILITY_DATA}. * @hide */ public static final int SERVICE_CAPABILITY_DATA_BITMASK = diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 3ff1e2ca8dfb..3e226ccf2737 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -6907,7 +6907,6 @@ public class TelephonyManager { return false; } - // TODO(b/316183370): replace all @code with @link in javadoc after feature is released /** * @return true if the current device is "voice capable". * <p> @@ -6921,10 +6920,10 @@ public class TelephonyManager { * PackageManager.FEATURE_TELEPHONY system feature, which is available * on any device with a telephony radio, even if the device is * data-only. - * @deprecated Replaced by {@code #isDeviceVoiceCapable()}. Starting from Android 15, voice + * @deprecated Replaced by {@link #isDeviceVoiceCapable()}. Starting from Android 15, voice * capability may also be overridden by carriers for a given subscription. For voice capable - * device (when {@code #isDeviceVoiceCapable} return {@code true}), caller should check for - * subscription-level voice capability as well. See {@code #isDeviceVoiceCapable} for details. + * device (when {@link #isDeviceVoiceCapable} return {@code true}), caller should check for + * subscription-level voice capability as well. See {@link #isDeviceVoiceCapable} for details. */ @Deprecated public boolean isVoiceCapable() { @@ -6946,8 +6945,8 @@ public class TelephonyManager { * <p> * Starting from Android 15, voice capability may also be overridden by carrier for a given * subscription on a voice capable device. To check if a subscription is "voice capable", - * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if - * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included. + * call method {@link SubscriptionInfo#getServiceCapabilities()} and check if + * {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ @@ -6964,10 +6963,10 @@ public class TelephonyManager { * <p> * Note: Voicemail waiting sms, cell broadcasting sms, and MMS are * disabled when device doesn't support sms. - * @deprecated Replaced by {@code #isDeviceSmsCapable()}. Starting from Android 15, SMS + * @deprecated Replaced by {@link #isDeviceSmsCapable()}. Starting from Android 15, SMS * capability may also be overridden by carriers for a given subscription. For SMS capable - * device (when {@code #isDeviceSmsCapable} return {@code true}), caller should check for - * subscription-level SMS capability as well. See {@code #isDeviceSmsCapable} for details. + * device (when {@link #isDeviceSmsCapable} return {@code true}), caller should check for + * subscription-level SMS capability as well. See {@link #isDeviceSmsCapable} for details. */ @Deprecated public boolean isSmsCapable() { @@ -6986,8 +6985,8 @@ public class TelephonyManager { * <p> * Starting from Android 15, SMS capability may also be overridden by carriers for a given * subscription on an SMS capable device. To check if a subscription is "SMS capable", - * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if - * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS} is included. + * call method {@link SubscriptionInfo#getServiceCapabilities()} and check if + * {@link SubscriptionManager#SERVICE_CAPABILITY_SMS} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 3944b8e0d0cc..284e2bd8aa6c 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -420,6 +420,14 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28; + /** + * Enabling satellite is in progress. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29; + /** @hide */ @IntDef(prefix = {"SATELLITE_RESULT_"}, value = { SATELLITE_RESULT_SUCCESS, @@ -450,7 +458,8 @@ public final class SatelliteManager { SATELLITE_RESULT_LOCATION_DISABLED, SATELLITE_RESULT_LOCATION_NOT_AVAILABLE, SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS, - SATELLITE_RESULT_DISABLE_IN_PROGRESS + SATELLITE_RESULT_DISABLE_IN_PROGRESS, + SATELLITE_RESULT_ENABLE_IN_PROGRESS }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteResult {} diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index e852e6bbb756..e57c207a0b3e 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3394,4 +3394,19 @@ interface ITelephony { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.SATELLITE_COMMUNICATION)") void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result); + + /** + * This API can be used by only CTS to override the cached value for the device overlay config + * value : + * config_satellite_gateway_service_package and + * config_satellite_carrier_roaming_esos_provisioned_class. + * These values are set before sending an intent to broadcast there are any change to list of + * subscriber informations. + * + * @param name the name is one of the following that constitute an intent. + * Component package name, or component class name. + * @return {@code true} if the setting is successful, {@code false} otherwise. + * @hide + */ + boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name); } diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java index ad0ef1b3a37f..0f08be215033 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -26,7 +26,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; import android.testing.TestableContext; import android.view.MotionEvent; import android.view.View; @@ -40,6 +42,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.cts.input.MotionEventBuilder; import com.android.cts.input.PointerBuilder; +import com.android.server.input.TouchpadFingerState; +import com.android.server.input.TouchpadHardwareState; import org.junit.Before; import org.junit.Test; @@ -289,4 +293,36 @@ public class TouchpadDebugViewTest { assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); } + + @Test + public void testTouchpadClick() { + View child; + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0])); + + for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) { + child = mTouchpadDebugView.getChildAt(i); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE); + } + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0])); + + for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) { + child = mTouchpadDebugView.getChildAt(i); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED); + } + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0])); + + for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) { + child = mTouchpadDebugView.getChildAt(i); + assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE); + } + } } diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index 27cc923a97db..e841d9ea0880 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -16,8 +16,6 @@ package com.android.internal.protolog; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -30,7 +28,6 @@ import static org.mockito.Mockito.when; import static java.io.File.createTempFile; -import android.content.Context; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.tools.ScenarioBuilder; @@ -45,6 +42,7 @@ import android.util.proto.ProtoInputStream; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.protolog.ProtoLogConfigurationService.ViewerConfigFileTracer; import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogDataType; import com.android.internal.protolog.common.LogLevel; @@ -53,11 +51,11 @@ import com.google.common.truth.Truth; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; import perfetto.protos.Protolog; import perfetto.protos.ProtologCommon; @@ -76,6 +74,7 @@ import java.util.concurrent.atomic.AtomicInteger; @RunWith(JUnit4.class) public class PerfettoProtoLogImplTest { private static final String TEST_PROTOLOG_DATASOURCE_NAME = "test.android.protolog"; + private static final String MOCK_VIEWER_CONFIG_FILE = "my/mock/viewer/config/file.pb"; private final File mTracingDirectory = InstrumentationRegistry.getInstrumentation() .getTargetContext().getFilesDir(); @@ -92,29 +91,19 @@ public class PerfettoProtoLogImplTest { new TraceConfig(false, true, false) ); - private ProtoLogConfigurationService mProtoLogConfigurationService; - private PerfettoProtoLogImpl mProtoLog; - private Protolog.ProtoLogViewerConfig.Builder mViewerConfigBuilder; - private File mFile; - private Runnable mCacheUpdater; + private static ProtoLogConfigurationService sProtoLogConfigurationService; + private static PerfettoProtoLogImpl sProtoLog; + private static Protolog.ProtoLogViewerConfig.Builder sViewerConfigBuilder; + private static Runnable sCacheUpdater; - private ProtoLogViewerConfigReader mReader; + private static ProtoLogViewerConfigReader sReader; public PerfettoProtoLogImplTest() throws IOException { } - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - final Context testContext = getInstrumentation().getContext(); - mFile = testContext.getFileStreamPath("tracing_test.dat"); - //noinspection ResultOfMethodCallIgnored - mFile.delete(); - - TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); - TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - - mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() + @BeforeClass + public static void setUp() throws Exception { + sViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() .addGroups( Protolog.ProtoLogViewerConfig.Group.newBuilder() .setId(1) @@ -160,33 +149,52 @@ public class PerfettoProtoLogImplTest { ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock( ViewerConfigInputStreamProvider.class); Mockito.when(viewerConfigInputStreamProvider.getInputStream()) - .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray())); + .thenAnswer(it -> new ProtoInputStream(sViewerConfigBuilder.build().toByteArray())); - mCacheUpdater = () -> {}; - mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); + sCacheUpdater = () -> {}; + sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); final ProtoLogDataSourceBuilder dataSourceBuilder = (onStart, onFlush, onStop) -> new ProtoLogDataSource( onStart, onFlush, onStop, TEST_PROTOLOG_DATASOURCE_NAME); - mProtoLogConfigurationService = - new ProtoLogConfigurationService(dataSourceBuilder); - mProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, mReader, () -> mCacheUpdater.run(), - TestProtoLogGroup.values(), dataSourceBuilder, mProtoLogConfigurationService); + final ViewerConfigFileTracer tracer = (dataSource, viewerConfigFilePath) -> { + Utils.dumpViewerConfig(dataSource, () -> { + if (!viewerConfigFilePath.equals(MOCK_VIEWER_CONFIG_FILE)) { + throw new RuntimeException( + "Unexpected viewer config file path provided"); + } + return new ProtoInputStream(sViewerConfigBuilder.build().toByteArray()); + }); + }; + sProtoLogConfigurationService = new ProtoLogConfigurationService(dataSourceBuilder, tracer); + + if (android.tracing.Flags.clientSideProtoLogging()) { + sProtoLog = new PerfettoProtoLogImpl( + MOCK_VIEWER_CONFIG_FILE, sReader, () -> sCacheUpdater.run(), + TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); + } else { + sProtoLog = new PerfettoProtoLogImpl( + viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(), + TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); + } + } + + @Before + public void before() { + Mockito.reset(sReader); + + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); + TestProtoLogGroup.TEST_GROUP.setLogToProto(false); } @After public void tearDown() { - if (mFile != null) { - //noinspection ResultOfMethodCallIgnored - mFile.delete(); - } ProtoLogImpl.setSingleInstance(null); } @Test public void isEnabled_returnsFalseByDefault() { - assertFalse(mProtoLog.isProtoEnabled()); + assertFalse(sProtoLog.isProtoEnabled()); } @Test @@ -196,7 +204,7 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); } finally { traceMonitor.stop(mWriter); } @@ -209,12 +217,12 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); } finally { traceMonitor.stop(mWriter); } - assertFalse(mProtoLog.isProtoEnabled()); + assertFalse(sProtoLog.isProtoEnabled()); } @Test @@ -226,15 +234,15 @@ public class PerfettoProtoLogImplTest { traceMonitor.start(); // Shouldn't be logging anything except WTF unless explicitly requested in the group // override. - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -258,15 +266,15 @@ public class PerfettoProtoLogImplTest { ).build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -294,15 +302,15 @@ public class PerfettoProtoLogImplTest { ).build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -324,15 +332,15 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, LogDataType.BOOLEAN, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -351,8 +359,8 @@ public class PerfettoProtoLogImplTest { @Test public void log_logcatEnabled() { - when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); @@ -363,13 +371,13 @@ public class PerfettoProtoLogImplTest { verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), eq("test true 10000 % 0x7530 test 3.0E-6")); - verify(mReader).getViewerString(eq(1234L)); + verify(sReader).getViewerString(eq(1234L)); } @Test public void log_logcatEnabledInvalidMessage() { - when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); @@ -381,29 +389,28 @@ public class PerfettoProtoLogImplTest { LogLevel.INFO), eq("FORMAT_ERROR \"test %b %d %% %x %s %f\", " + "args=(true, 10000, 1.0E-4, 2.0E-5, test)")); - verify(mReader).getViewerString(eq(1234L)); + verify(sReader).getViewerString(eq(1234L)); } @Test public void log_logcatEnabledNoMessage() { - when(mReader.getViewerString(anyLong())).thenReturn(null); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn(null); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)")); - verify(mReader).getViewerString(eq(1234L)); + verify(sReader).getViewerString(eq(1234L)); } @Test public void log_logcatDisabled() { - when(mReader.getViewerString(anyLong())).thenReturn("test %d"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn("test %d"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); implSpy.log( @@ -411,7 +418,7 @@ public class PerfettoProtoLogImplTest { new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); - verify(mReader, never()).getViewerString(anyLong()); + verify(sReader, never()).getViewerString(anyLong()); } @Test @@ -426,11 +433,12 @@ public class PerfettoProtoLogImplTest { long before; long after; try { + assertFalse(sProtoLog.isProtoEnabled()); traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( + sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); @@ -448,7 +456,8 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) .isAtMost(after); Truth.assertThat(protolog.messages.getFirst().getMessage()) - .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true"); + .isEqualTo( + "My test message :: test, 1, 2, 3, 0.400000, 5.000000e-01, 0.6, true"); } @Test @@ -460,10 +469,10 @@ public class PerfettoProtoLogImplTest { long after; try { traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( + sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, "My test message :: %s, %d, %x, %f, %b", "test", 1, 3, 0.4, true); @@ -481,7 +490,7 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) .isAtMost(after); Truth.assertThat(protolog.messages.getFirst().getMessage()) - .isEqualTo("My test message :: test, 2, 6, 0.400000, true"); + .isEqualTo("My test message :: test, 1, 3, 0.400000, true"); } @Test @@ -491,7 +500,7 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -507,7 +516,7 @@ public class PerfettoProtoLogImplTest { private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) { final long messageId = new Random().nextLong(); - mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + sViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(messageId) .setMessage(message) .setLevel(logLevel) @@ -530,7 +539,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( + sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, 0b01100100, new Object[]{"test", 1, 0.1, true}); @@ -550,7 +559,7 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, 0b11, new Object[]{true}); } finally { traceMonitor.stop(mWriter); @@ -575,7 +584,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); - ProtoLogImpl.setSingleInstance(mProtoLog); + ProtoLogImpl.setSingleInstance(sProtoLog); ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1, 0b11, true); } finally { @@ -599,7 +608,7 @@ public class PerfettoProtoLogImplTest { @Test public void cacheIsUpdatedWhenTracesStartAndStop() { final AtomicInteger cacheUpdateCallCount = new AtomicInteger(0); - mCacheUpdater = cacheUpdateCallCount::incrementAndGet; + sCacheUpdater = cacheUpdateCallCount::incrementAndGet; PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() .enableProtoLog(true, @@ -641,17 +650,17 @@ public class PerfettoProtoLogImplTest { @Test public void isEnabledUpdatesBasedOnRunningTraces() { - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse(); + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse(); PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, @@ -670,65 +679,65 @@ public class PerfettoProtoLogImplTest { try { traceMonitor1.start(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); try { traceMonitor2.start(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)).isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); } finally { traceMonitor2.stop(mWriter); } - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); } finally { traceMonitor1.stop(mWriter); } - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isFalse(); } @@ -741,7 +750,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, "My test null string: %s", (Object) null); } finally { traceMonitor.stop(mWriter); @@ -764,7 +773,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, "My null args: %d, %f, %b", null, null, null); } finally { traceMonitor.stop(mWriter); @@ -775,7 +784,7 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(protolog.messages).hasSize(1); Truth.assertThat(protolog.messages.get(0).getMessage()) - .isEqualTo("My null args: 0, 0, false"); + .isEqualTo("My null args: 0, 0.000000, false"); } @Test @@ -798,7 +807,7 @@ public class PerfettoProtoLogImplTest { traceMonitor1.start(); traceMonitor2.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor1.stop(mWriter); @@ -827,12 +836,12 @@ public class PerfettoProtoLogImplTest { .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, - "This message should not be logged"); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, - "This message should logged %d", 123); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, - "This message should also be logged %d", 567); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "This message should not be logged"); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, + "This message should be logged %d", 123); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, + "This message should also be logged %d", 567); } finally { traceMonitor.stop(mWriter); } @@ -845,7 +854,7 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(protolog.messages.get(0).getLevel()) .isEqualTo(LogLevel.WARN); Truth.assertThat(protolog.messages.get(0).getMessage()) - .isEqualTo("This message should logged 123"); + .isEqualTo("This message should be logged 123"); Truth.assertThat(protolog.messages.get(1).getLevel()) .isEqualTo(LogLevel.ERROR); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index 0439d5f54e23..edad67896e8e 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -123,7 +123,6 @@ public abstract class NetworkEvaluationTestBase { mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS); mSetFlagsRule.enableFlags(Flags.FLAG_EVALUATE_IPSEC_LOSS_ON_LP_NC_CHANGE); mSetFlagsRule.enableFlags(Flags.FLAG_HANDLE_SEQ_NUM_LEAP); - mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_DISABLE_IPSEC_LOSS_DETECTOR); when(mNetwork.getNetId()).thenReturn(-1); diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp index 5e0f87f0dcaf..60c4bf5c4131 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -292,13 +292,12 @@ bool processFile(Bundle* bundle, ZipFile* zip, } if (!hasData) { const String8& srcName = file->getSourceFile(); - time_t fileModWhen; - fileModWhen = getFileModDate(srcName.c_str()); - if (fileModWhen == (time_t) -1) { // file existence tested earlier, - return false; // not expecting an error here + auto fileModWhen = getFileModDate(srcName.c_str()); + if (fileModWhen == kInvalidModDate) { // file existence tested earlier, + return false; // not expecting an error here } - - if (fileModWhen > entry->getModWhen()) { + + if (toTimeT(fileModWhen) > entry->getModWhen()) { // mark as deleted so add() will succeed if (bundle->getVerbose()) { printf(" (removing old '%s')\n", storageName.c_str()); diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java new file mode 100644 index 000000000000..bc9471b84b97 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open 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.hosttest.annotation; + +import static java.lang.annotation.ElementType.METHOD; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY + * QUESTIONS ABOUT IT. + * @hide + */ +@Target({METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface HostSideTestRedirect { +} diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java index 9c8138351eb5..28ad236a66f3 100644 --- a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java +++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java @@ -30,6 +30,6 @@ import java.lang.annotation.Target; */ @Target({TYPE}) @Retention(RetentionPolicy.CLASS) -public @interface HostSideTestNativeSubstitutionClass { +public @interface HostSideTestRedirectionClass { String value(); } diff --git a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt index e72c9a41d796..eba8e62c7270 100644 --- a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt +++ b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt @@ -27,8 +27,11 @@ --substitute-annotation android.hosttest.annotation.HostSideTestSubstitute ---native-substitute-annotation - android.hosttest.annotation.HostSideTestNativeSubstitutionClass +--redirect-annotation + android.hosttest.annotation.HostSideTestRedirect + +--redirection-class-annotation + android.hosttest.annotation.HostSideTestRedirectionClass --class-load-hook-annotation android.hosttest.annotation.HostSideTestClassLoadHook diff --git a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh index 5f0368a48c09..084448d0a797 100755 --- a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh +++ b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh @@ -100,8 +100,10 @@ run_hoststubgen() { android.hosttest.annotation.HostSideTestRemove \ --substitute-annotation \ android.hosttest.annotation.HostSideTestSubstitute \ - --native-substitute-annotation \ - android.hosttest.annotation.HostSideTestNativeSubstitutionClass \ + --redirect-annotation \ + android.hosttest.annotation.HostSideTestRedirect \ + --redirection-class-annotation \ + android.hosttest.annotation.HostSideTestRedirectionClass \ --class-load-hook-annotation \ android.hosttest.annotation.HostSideTestClassLoadHook \ --keep-static-initializer-annotation \ @@ -223,4 +225,4 @@ EXTRA_ARGS="--in-jar abc" run_hoststubgen_for_failure "Duplicate arg" \ echo "All tests passed" -exit 0
\ No newline at end of file +exit 0 diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 0f38fe7d5068..34aaaa9cfa9f 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -24,8 +24,9 @@ import com.android.hoststubgen.filters.DefaultHookInjectingFilter import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.filters.FilterRemapper import com.android.hoststubgen.filters.ImplicitOutputFilter -import com.android.hoststubgen.filters.NativeFilter +import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter +import com.android.hoststubgen.filters.SanitizationFilter import com.android.hoststubgen.filters.createFilterFromTextPolicyFile import com.android.hoststubgen.filters.printAsTextPolicy import com.android.hoststubgen.utils.ClassFilter @@ -134,7 +135,7 @@ class HostStubGen(val options: HostStubGenOptions) { var filter: OutputFilter = ConstantFilter(options.defaultPolicy.get, "default-by-options") // Next, we build a filter that preserves all native methods by default - filter = NativeFilter(allClasses, filter) + filter = KeepNativeFilter(allClasses, filter) // Next, we need a filter that resolves "class-wide" policies. // This is used when a member (methods, fields, nested classes) don't get any polices @@ -166,11 +167,12 @@ class HostStubGen(val options: HostStubGenOptions) { options.throwAnnotations, options.removeAnnotations, options.substituteAnnotations, - options.nativeSubstituteAnnotations, + options.redirectAnnotations, + options.redirectionClassAnnotations, options.classLoadHookAnnotations, options.keepStaticInitializerAnnotations, annotationAllowedClassesFilter, - filter, + filter ) // Next, "text based" filter, which allows to override polices without touching @@ -182,6 +184,9 @@ class HostStubGen(val options: HostStubGenOptions) { // Apply the implicit filter. filter = ImplicitOutputFilter(errors, allClasses, filter) + // Add a final sanitization step. + filter = SanitizationFilter(errors, allClasses, filter) + return filter } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt index 1cedcc349c51..057a52cc06d0 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt @@ -85,9 +85,10 @@ class HostStubGenOptions( var throwAnnotations: MutableSet<String> = mutableSetOf(), var removeAnnotations: MutableSet<String> = mutableSetOf(), var keepClassAnnotations: MutableSet<String> = mutableSetOf(), + var redirectAnnotations: MutableSet<String> = mutableSetOf(), var substituteAnnotations: MutableSet<String> = mutableSetOf(), - var nativeSubstituteAnnotations: MutableSet<String> = mutableSetOf(), + var redirectionClassAnnotations: MutableSet<String> = mutableSetOf(), var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(), var keepStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(), @@ -186,8 +187,11 @@ class HostStubGenOptions( "--substitute-annotation" -> ret.substituteAnnotations.addUniqueAnnotationArg() - "--native-substitute-annotation" -> - ret.nativeSubstituteAnnotations.addUniqueAnnotationArg() + "--redirect-annotation" -> + ret.redirectAnnotations.addUniqueAnnotationArg() + + "--redirection-class-annotation" -> + ret.redirectionClassAnnotations.addUniqueAnnotationArg() "--class-load-hook-annotation" -> ret.classLoadHookAnnotations.addUniqueAnnotationArg() @@ -275,7 +279,7 @@ class HostStubGenOptions( removeAnnotations=$removeAnnotations, keepClassAnnotations=$keepClassAnnotations, substituteAnnotations=$substituteAnnotations, - nativeSubstituteAnnotations=$nativeSubstituteAnnotations, + nativeSubstituteAnnotations=$redirectionClassAnnotations, classLoadHookAnnotations=$classLoadHookAnnotations, keepStaticInitializerAnnotations=$keepStaticInitializerAnnotations, packageRedirects=$packageRedirects, diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index 7197e0e79861..a02082d12934 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -29,33 +29,24 @@ import org.objectweb.asm.tree.MethodNode /** Name of the class initializer method. */ -val CLASS_INITIALIZER_NAME = "<clinit>" +const val CLASS_INITIALIZER_NAME = "<clinit>" /** Descriptor of the class initializer method. */ -val CLASS_INITIALIZER_DESC = "()V" +const val CLASS_INITIALIZER_DESC = "()V" /** Name of constructors. */ -val CTOR_NAME = "<init>" +const val CTOR_NAME = "<init>" /** - * Find any of [anyAnnotations] from the list of visible / invisible annotations. + * Find any of [set] from the list of visible / invisible annotations. */ fun findAnyAnnotation( - anyAnnotations: Set<String>, - visibleAnnotations: List<AnnotationNode>?, - invisibleAnnotations: List<AnnotationNode>?, - ): AnnotationNode? { - for (an in visibleAnnotations ?: emptyList()) { - if (anyAnnotations.contains(an.desc)) { - return an - } - } - for (an in invisibleAnnotations ?: emptyList()) { - if (anyAnnotations.contains(an.desc)) { - return an - } - } - return null + set: Set<String>, + visibleAnnotations: List<AnnotationNode>?, + invisibleAnnotations: List<AnnotationNode>?, +): AnnotationNode? { + return visibleAnnotations?.find { it.desc in set } + ?: invisibleAnnotations?.find { it.desc in set } } fun ClassNode.findAnyAnnotation(set: Set<String>): AnnotationNode? { @@ -70,6 +61,27 @@ fun FieldNode.findAnyAnnotation(set: Set<String>): AnnotationNode? { return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations) } +fun findAllAnnotations( + set: Set<String>, + visibleAnnotations: List<AnnotationNode>?, + invisibleAnnotations: List<AnnotationNode>? +): List<AnnotationNode> { + return (visibleAnnotations ?: emptyList()).filter { it.desc in set } + + (invisibleAnnotations ?: emptyList()).filter { it.desc in set } +} + +fun ClassNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> { + return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations) +} + +fun MethodNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> { + return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations) +} + +fun FieldNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> { + return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations) +} + fun <T> findAnnotationValueAsObject( an: AnnotationNode, propertyName: String, diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt index 38a41b26dcfc..a6b8cdb0c80b 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt @@ -18,12 +18,15 @@ package com.android.hoststubgen.filters import com.android.hoststubgen.ClassParseException import com.android.hoststubgen.HostStubGenErrors import com.android.hoststubgen.InvalidAnnotationException -import com.android.hoststubgen.addNonNullElement +import com.android.hoststubgen.addLists import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.findAllAnnotations import com.android.hoststubgen.asm.findAnnotationValueAsString import com.android.hoststubgen.asm.findAnyAnnotation +import com.android.hoststubgen.asm.getPackageNameFromFullClassName +import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.asm.toHumanReadableMethodName import com.android.hoststubgen.asm.toJvmClassName @@ -46,7 +49,8 @@ class AnnotationBasedFilter( throwAnnotations_: Set<String>, removeAnnotations_: Set<String>, substituteAnnotations_: Set<String>, - nativeSubstituteAnnotations_: Set<String>, + redirectAnnotations_: Set<String>, + redirectionClassAnnotations_: Set<String>, classLoadHookAnnotations_: Set<String>, keepStaticInitializerAnnotations_: Set<String>, private val annotationAllowedClassesFilter: ClassFilter, @@ -56,8 +60,10 @@ class AnnotationBasedFilter( private val keepClassAnnotations = convertToInternalNames(keepClassAnnotations_) private val throwAnnotations = convertToInternalNames(throwAnnotations_) private val removeAnnotations = convertToInternalNames(removeAnnotations_) + private val redirectAnnotations = convertToInternalNames(redirectAnnotations_) private val substituteAnnotations = convertToInternalNames(substituteAnnotations_) - private val nativeSubstituteAnnotations = convertToInternalNames(nativeSubstituteAnnotations_) + private val redirectionClassAnnotations = + convertToInternalNames(redirectionClassAnnotations_) private val classLoadHookAnnotations = convertToInternalNames(classLoadHookAnnotations_) private val keepStaticInitializerAnnotations = convertToInternalNames(keepStaticInitializerAnnotations_) @@ -67,11 +73,12 @@ class AnnotationBasedFilter( keepClassAnnotations + throwAnnotations + removeAnnotations + + redirectAnnotations + substituteAnnotations /** All the annotations we use. */ private val allAnnotations = visibilityAnnotations + - nativeSubstituteAnnotations + + redirectionClassAnnotations + classLoadHookAnnotations + keepStaticInitializerAnnotations @@ -84,8 +91,9 @@ class AnnotationBasedFilter( keepClassAnnotations_ + throwAnnotations_ + removeAnnotations_ + + redirectAnnotations_ + substituteAnnotations_ + - nativeSubstituteAnnotations_ + + redirectionClassAnnotations_ + classLoadHookAnnotations_ + keepStaticInitializerAnnotations_ ) @@ -99,6 +107,7 @@ class AnnotationBasedFilter( in substituteAnnotations -> FilterPolicy.Substitute.withReason(REASON_ANNOTATION) in throwAnnotations -> FilterPolicy.Throw.withReason(REASON_ANNOTATION) in removeAnnotations -> FilterPolicy.Remove.withReason(REASON_ANNOTATION) + in redirectAnnotations -> FilterPolicy.Redirect.withReason(REASON_ANNOTATION) else -> null } } @@ -129,13 +138,6 @@ class AnnotationBasedFilter( descriptor: String ): FilterPolicyWithReason { val cn = classes.getClass(className) - - if (methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) { - if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) { - return FilterPolicy.Keep.withReason(REASON_ANNOTATION) - } - } - return getAnnotationPolicy(cn).methodPolicies[MethodKey(methodName, descriptor)] ?: super.getPolicyForMethod(className, methodName, descriptor) } @@ -150,22 +152,14 @@ class AnnotationBasedFilter( ?: super.getRenameTo(className, methodName, descriptor) } - override fun getNativeSubstitutionClass(className: String): String? { - classes.getClass(className).let { cn -> - cn.findAnyAnnotation(nativeSubstituteAnnotations)?.let { an -> - return getAnnotationField(an, "value")?.toJvmClassName() - } - } - return null + override fun getRedirectionClass(className: String): String? { + val cn = classes.getClass(className) + return getAnnotationPolicy(cn).redirectionClass } override fun getClassLoadHooks(className: String): List<String> { - val e = classes.getClass(className).let { cn -> - cn.findAnyAnnotation(classLoadHookAnnotations)?.let { an -> - getAnnotationField(an, "value")?.toHumanReadableMethodName() - } - } - return addNonNullElement(super.getClassLoadHooks(className), e) + val cn = classes.getClass(className) + return addLists(super.getClassLoadHooks(className), getAnnotationPolicy(cn).classLoadHooks) } private data class MethodKey(val name: String, val desc: String) @@ -195,6 +189,8 @@ class AnnotationBasedFilter( val fieldPolicies = mutableMapOf<String, FilterPolicyWithReason>() val methodPolicies = mutableMapOf<MethodKey, FilterPolicyWithReason>() val renamedMethods = mutableMapOf<MethodKey, String>() + val redirectionClass: String? + val classLoadHooks: List<String> init { val allowAnnotation = annotationAllowedClassesFilter.matches(cn.name) @@ -204,6 +200,16 @@ class AnnotationBasedFilter( "class", cn.name ) classPolicy = cn.findAnyAnnotation(visibilityAnnotations)?.policy + redirectionClass = cn.findAnyAnnotation(redirectionClassAnnotations)?.let { an -> + getAnnotationField(an, "value")?.let { resolveRelativeClass(cn, it) } + } + classLoadHooks = cn.findAllAnnotations(classLoadHookAnnotations).mapNotNull { an -> + getAnnotationField(an, "value")?.toHumanReadableMethodName() + } + if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) { + methodPolicies[MethodKey(CLASS_INITIALIZER_NAME, CLASS_INITIALIZER_DESC)] = + FilterPolicy.Keep.withReason(REASON_ANNOTATION) + } for (fn in cn.fields ?: emptyList()) { detectInvalidAnnotations( @@ -297,25 +303,36 @@ class AnnotationBasedFilter( ) } } - } - /** - * Return the (String) value of 'value' parameter from an annotation. - */ - private fun getAnnotationField( - an: AnnotationNode, - name: String, - required: Boolean = true - ): String? { - try { - val suffix = findAnnotationValueAsString(an, name) - if (suffix == null && required) { - errors.onErrorFound("Annotation \"${an.desc}\" must have field $name") + /** + * Return the (String) value of 'value' parameter from an annotation. + */ + private fun getAnnotationField( + an: AnnotationNode, + name: String, + required: Boolean = true + ): String? { + try { + val suffix = findAnnotationValueAsString(an, name) + if (suffix == null && required) { + errors.onErrorFound("Annotation \"${an.desc}\" must have field $name") + } + return suffix + } catch (e: ClassParseException) { + errors.onErrorFound(e.message!!) + return null } - return suffix - } catch (e: ClassParseException) { - errors.onErrorFound(e.message!!) - return null + } + + /** + * Resolve the full class name if the class is relative + */ + private fun resolveRelativeClass( + cn: ClassNode, + name: String + ): String { + val packageName = getPackageNameFromFullClassName(cn.name) + return resolveClassNameWithDefaultPackage(name, packageName).toJvmClassName() } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt index 8ee3a946a21c..f8bb526d0a86 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt @@ -16,7 +16,6 @@ package com.android.hoststubgen.filters import com.android.hoststubgen.asm.ClassNodes -import com.android.hoststubgen.asm.isNative /** * This is used as the second last fallback filter. This filter propagates the class-wide policy @@ -88,16 +87,7 @@ class ClassWidePolicyPropagatingFilter( methodName: String, descriptor: String ): FilterPolicyWithReason { - return outermostFilter.getNativeSubstitutionClass(className)?.let { - // First check native substitution - classes.findMethod(className, methodName, descriptor)?.let { mn -> - if (mn.isNative()) { - FilterPolicy.NativeSubstitute.withReason("class-wide in $className") - } else { - null - } - } - } ?: getClassWidePolicy(className, resolve = true) - ?: super.getPolicyForMethod(className, methodName, descriptor) + return getClassWidePolicy(className, resolve = true) + ?: super.getPolicyForMethod(className, methodName, descriptor) } -}
\ No newline at end of file +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt index 6fcffb89924a..b8b0d8a31268 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt @@ -72,8 +72,8 @@ abstract class DelegatingFilter( return fallback.getRenameTo(className, methodName, descriptor) } - override fun getNativeSubstitutionClass(className: String): String? { - return fallback.getNativeSubstitutionClass(className) + override fun getRedirectionClass(className: String): String? { + return fallback.getRedirectionClass(className) } override fun getClassLoadHooks(className: String): List<String> { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt index ab03874cf43d..2f2f81b05ad1 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt @@ -33,9 +33,9 @@ enum class FilterPolicy { Substitute, /** - * Only usable with methods. Replace a native method with a "substitution" method, + * Only usable with methods. Redirect a method to a method in the substitution class. */ - NativeSubstitute, + Redirect, /** * Only usable with methods. The item will be kept in the impl jar file, but when called, @@ -102,8 +102,7 @@ enum class FilterPolicy { val isSupported: Boolean get() { return when (this) { - // TODO: handle native method with no substitution as being unsupported - Keep, KeepClass, Substitute, NativeSubstitute -> true + Keep, KeepClass, Substitute, Redirect -> true else -> false } } @@ -111,7 +110,7 @@ enum class FilterPolicy { val isMethodRewriteBody: Boolean get() { return when (this) { - NativeSubstitute, Throw, Ignore -> true + Redirect, Throw, Ignore -> true else -> false } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt index 2e144f5513bc..59fa464a7212 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt @@ -19,6 +19,7 @@ import com.android.hoststubgen.addNonNullElement import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.asm.toHumanReadableMethodName +import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log // TODO: Validate all input names. @@ -29,7 +30,7 @@ class InMemoryOutputFilter( ) : DelegatingFilter(fallback) { private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf() private val mRenames: MutableMap<String, String> = mutableMapOf() - private val mNativeSubstitutionClasses: MutableMap<String, String> = mutableMapOf() + private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf() private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf() private fun getClassKey(className: String): String { @@ -115,17 +116,17 @@ class InMemoryOutputFilter( mRenames[getMethodKey(className, methodName, descriptor)] = toName } - override fun getNativeSubstitutionClass(className: String): String? { - return mNativeSubstitutionClasses[getClassKey(className)] - ?: super.getNativeSubstitutionClass(className) + override fun getRedirectionClass(className: String): String? { + return mRedirectionClasses[getClassKey(className)] + ?: super.getRedirectionClass(className) } - fun setNativeSubstitutionClass(from: String, to: String) { + fun setRedirectionClass(from: String, to: String) { checkClass(from) - // Native substitute classes may be provided from other jars, so we can't do this check. + // Redirection classes may be provided from other jars, so we can't do this check. // ensureClassExists(to) - mNativeSubstitutionClasses[getClassKey(from)] = to.toHumanReadableClassName() + mRedirectionClasses[getClassKey(from)] = to.toJvmClassName() } override fun getClassLoadHooks(className: String): List<String> { @@ -136,4 +137,4 @@ class InMemoryOutputFilter( fun setClassLoadHook(className: String, methodName: String) { mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName() } -}
\ No newline at end of file +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt index bd719310719b..00e7d77fa6e7 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt @@ -18,7 +18,12 @@ package com.android.hoststubgen.filters import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.isNative -class NativeFilter( +/** + * For native methods that weren't handled by outer filters, we keep it so that + * native method registration will not crash at runtime. Ideally we shouldn't need + * this, but in practice unsupported native method registrations do occur. + */ +class KeepNativeFilter( private val classes: ClassNodes, fallback: OutputFilter ) : DelegatingFilter(fallback) { @@ -28,8 +33,6 @@ class NativeFilter( descriptor: String, ): FilterPolicyWithReason { return classes.findMethod(className, methodName, descriptor)?.let { mn -> - // For native methods that weren't handled by outer filters, - // we keep it so that native method registration will not crash. if (mn.isNative()) { FilterPolicy.Keep.withReason("native-preserve") } else { diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt index 1049e2bf94cf..f99ce906240a 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt @@ -35,10 +35,6 @@ abstract class OutputFilter { * using it. */ open var outermostFilter: OutputFilter = this - get() = field - set(value) { - field = value - } abstract fun getPolicyForClass(className: String): FilterPolicyWithReason @@ -60,13 +56,13 @@ abstract class OutputFilter { } /** - * Return a "native substitution class" name for a given class. + * Return a "redirection class" name for a given class. * - * The result will be in a "human readable" form. (e.g. uses '.'s instead of '/'s) + * The result will be in a JVM internal form. (e.g. uses '/'s instead of '.'s) * - * (which corresponds to @HostSideTestNativeSubstitutionClass of the standard annotations.) + * (which corresponds to @HostSideTestRedirectClass of the standard annotations.) */ - open fun getNativeSubstitutionClass(className: String): String? { + open fun getRedirectionClass(className: String): String? { return null } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt new file mode 100644 index 000000000000..18a1e16bcf3a --- /dev/null +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open 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.hoststubgen.filters + +import com.android.hoststubgen.HostStubGenErrors +import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.toHumanReadableClassName +import com.android.hoststubgen.log + +/** + * Check whether the policies in the inner layers make sense, and sanitize the results. + */ +class SanitizationFilter( + private val errors: HostStubGenErrors, + private val classes: ClassNodes, + fallback: OutputFilter +) : DelegatingFilter(fallback) { + override fun getPolicyForMethod( + className: String, + methodName: String, + descriptor: String + ): FilterPolicyWithReason { + val policy = super.getPolicyForMethod(className, methodName, descriptor) + if (policy.policy == FilterPolicy.Redirect) { + // Check whether the hosting class has a redirection class + if (getRedirectionClass(className) == null) { + errors.onErrorFound("Method $methodName$descriptor requires a redirection " + + "class set on ${className.toHumanReadableClassName()}") + } + } + return policy + } + + override fun getRedirectionClass(className: String): String? { + return super.getRedirectionClass(className)?.also { clazz -> + if (classes.findClass(clazz) == null) { + log.w("Redirection class $clazz not found. Class must be available at runtime.") + } else if (outermostFilter.getPolicyForClass(clazz).policy != FilterPolicy.KeepClass) { + // If the class exists, it must have a KeepClass policy. + errors.onErrorFound("Redirection class $clazz must have @KeepWholeClass.") + } + } + } +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 14fd82b271e1..073b503401b5 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -142,9 +142,9 @@ fun createFilterFromTextPolicyFile( throw ParseException( "Special class can't have a substitution") } - // It's a native-substitution. + // It's a redirection class. val toClass = fields[2].substring(1) - imf.setNativeSubstitutionClass(className, toClass) + imf.setRedirectionClass(className, toClass) } else if (fields[2].startsWith("~")) { if (classType != SpecialClass.NotSpecial) { // We could support it, but not needed at least for now. @@ -350,6 +350,7 @@ private fun parsePolicy(s: String): FilterPolicy { "r", "remove" -> FilterPolicy.Remove "kc", "keepclass" -> FilterPolicy.KeepClass "i", "ignore" -> FilterPolicy.Ignore + "rdr", "redirect" -> FilterPolicy.Redirect else -> { if (s.startsWith("@")) { FilterPolicy.Substitute diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt index 41ba9286ef1e..261ef59c45c7 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt @@ -21,8 +21,6 @@ import com.android.hoststubgen.LogLevel import com.android.hoststubgen.asm.ClassNodes import com.android.hoststubgen.asm.UnifiedVisitor import com.android.hoststubgen.asm.getPackageNameFromFullClassName -import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage -import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.filters.FilterPolicy import com.android.hoststubgen.filters.FilterPolicyWithReason import com.android.hoststubgen.filters.OutputFilter @@ -57,7 +55,7 @@ abstract class BaseAdapter( protected lateinit var currentPackageName: String protected lateinit var currentClassName: String - protected var nativeSubstitutionClass: String? = null + protected var redirectionClass: String? = null protected lateinit var classPolicy: FilterPolicyWithReason override fun visit( @@ -72,34 +70,13 @@ abstract class BaseAdapter( currentClassName = name currentPackageName = getPackageNameFromFullClassName(name) classPolicy = filter.getPolicyForClass(currentClassName) + redirectionClass = filter.getRedirectionClass(currentClassName) log.d("[%s] visit: %s (package: %s)", this.javaClass.simpleName, name, currentPackageName) log.indent() log.v("Emitting class: %s", name) log.indent() - filter.getNativeSubstitutionClass(currentClassName)?.let { className -> - val fullClassName = resolveClassNameWithDefaultPackage(className, currentPackageName) - .toJvmClassName() - log.d(" NativeSubstitutionClass: $fullClassName") - if (classes.findClass(fullClassName) == null) { - log.w( - "Native substitution class $fullClassName not found. Class must be " + - "available at runtime." - ) - } else { - // If the class exists, it must have a KeepClass policy. - if (filter.getPolicyForClass(fullClassName).policy != FilterPolicy.KeepClass) { - // TODO: Use real annotation name. - options.errors.onErrorFound( - "Native substitution class $fullClassName should have @Keep." - ) - } - } - - nativeSubstitutionClass = fullClassName - } - // Inject annotations to generated classes. UnifiedVisitor.on(this).visitAnnotation(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, true) } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt index 057d653d7c45..567a69e43b58 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt @@ -184,9 +184,12 @@ class ImplGeneratingAdapter( return IgnoreMethodAdapter(descriptor, forceCreateBody, innerVisitor) .withAnnotation(HostStubGenProcessedAsIgnore.CLASS_DESCRIPTOR) } - FilterPolicy.NativeSubstitute -> { - log.v("Rewriting native method...") - return NativeSubstitutingMethodAdapter(access, name, descriptor, innerVisitor) + FilterPolicy.Redirect -> { + log.v("Redirecting method...") + return RedirectMethodAdapter( + access, name, descriptor, + forceCreateBody, innerVisitor + ) .withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR) } else -> {} @@ -274,15 +277,16 @@ class ImplGeneratingAdapter( } /** - * A method adapter that rewrite a native method body with a - * call to a method in the "native substitution" class. + * A method adapter that rewrite a method body with a + * call to a method in the redirection class. */ - private inner class NativeSubstitutingMethodAdapter( + private inner class RedirectMethodAdapter( access: Int, private val name: String, private val descriptor: String, + createBody: Boolean, next: MethodVisitor? - ) : BodyReplacingMethodVisitor(true, next) { + ) : BodyReplacingMethodVisitor(createBody, next) { private val isStatic = (access and Opcodes.ACC_STATIC) != 0 @@ -290,7 +294,7 @@ class ImplGeneratingAdapter( var targetDescriptor = descriptor var argOffset = 0 - // For non-static native method, we need to tweak it a bit. + // For non-static method, we need to tweak it a bit. if (!isStatic) { // Push `this` as the first argument. this.visitVarInsn(Opcodes.ALOAD, 0) @@ -310,7 +314,7 @@ class ImplGeneratingAdapter( visitMethodInsn( INVOKESTATIC, - nativeSubstitutionClass, + redirectionClass, name, targetDescriptor, false diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt index 5fde14ff525f..82586bb9fcdc 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt @@ -41,20 +41,40 @@ RuntimeVisibleAnnotations: java.lang.annotation.Retention( value=Ljava/lang/annotation/RetentionPolicy;.CLASS ) -## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class - Compiled from "HostSideTestNativeSubstitutionClass.java" -public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation +## Class: android/hosttest/annotation/HostSideTestRedirect.class + Compiled from "HostSideTestRedirect.java" +public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation minor version: 0 major version: 61 flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION - this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass + this_class: #x // android/hosttest/annotation/HostSideTestRedirect + super_class: #x // java/lang/Object + interfaces: 1, fields: 0, methods: 0, attributes: 2 +} +SourceFile: "HostSideTestRedirect.java" +RuntimeVisibleAnnotations: + x: #x(#x=[e#x.#x]) + java.lang.annotation.Target( + value=[Ljava/lang/annotation/ElementType;.METHOD] + ) + x: #x(#x=e#x.#x) + java.lang.annotation.Retention( + value=Ljava/lang/annotation/RetentionPolicy;.CLASS + ) +## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class + Compiled from "HostSideTestRedirectionClass.java" +public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation + minor version: 0 + major version: 61 + flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION + this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass super_class: #x // java/lang/Object interfaces: 1, fields: 0, methods: 1, attributes: 2 public abstract java.lang.String value(); descriptor: ()Ljava/lang/String; flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT } -SourceFile: "HostSideTestNativeSubstitutionClass.java" +SourceFile: "HostSideTestRedirectionClass.java" RuntimeVisibleAnnotations: x: #x(#x=[e#x.#x]) java.lang.annotation.Target( @@ -1925,7 +1945,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 12, attributes: 2 + interfaces: 0, fields: 1, methods: 14, attributes: 2 int value; descriptor: I flags: (0x0000) @@ -1946,6 +1966,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative public static native int nativeAddTwo(int); descriptor: (I)I flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static int nativeAddTwo_should_be_like_this(int); descriptor: (I)I @@ -1963,6 +1986,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative public static native long nativeLongPlus(long, long); descriptor: (JJ)J flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static long nativeLongPlus_should_be_like_this(long, long); descriptor: (JJ)J @@ -1997,6 +2023,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative public native int nativeNonStaticAddToValue(int); descriptor: (I)I flags: (0x0101) ACC_PUBLIC, ACC_NATIVE + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public int nativeNonStaticAddToValue_should_be_like_this(int); descriptor: (I)I @@ -2023,9 +2052,6 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative public static native void nativeStillKeep(); descriptor: ()V flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE - RuntimeInvisibleAnnotations: - x: #x() - android.hosttest.annotation.HostSideTestKeep public static void nativeStillNotSupported_should_be_like_this(); descriptor: ()V @@ -2041,13 +2067,47 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative public static native byte nativeBytePlus(byte, byte); descriptor: (BB)B flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public void notNativeRedirected(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=2, locals=1, args_size=1 + x: new #x // class java/lang/RuntimeException + x: dup + x: invokespecial #x // Method java/lang/RuntimeException."<init>":()V + x: athrow + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 8 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative; + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=2, locals=0, args_size=0 + x: new #x // class java/lang/RuntimeException + x: dup + x: invokespecial #x // Method java/lang/RuntimeException."<init>":()V + x: athrow + LineNumberTable: + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect } SourceFile: "TinyFrameworkNative.java" RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassKeep x: #x(#x=s#x) - android.hosttest.annotation.HostSideTestNativeSubstitutionClass( + android.hosttest.annotation.HostSideTestRedirectionClass( value="TinyFrameworkNative_host" ) ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class @@ -2058,7 +2118,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 5, attributes: 2 + interfaces: 0, fields: 0, methods: 7, attributes: 2 public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host(); descriptor: ()V flags: (0x0001) ACC_PUBLIC @@ -2132,6 +2192,25 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host Start Length Slot Name Signature 0 5 0 arg1 B 0 5 1 arg2 B + + public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative); + descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=0, locals=1, args_size=1 + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative; + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=0, locals=0, args_size=0 + x: return + LineNumberTable: } SourceFile: "TinyFrameworkNative_host.java" RuntimeInvisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt index e41d46d4daaa..31bbcc57ca9c 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt @@ -48,13 +48,35 @@ RuntimeVisibleAnnotations: java.lang.annotation.Retention( value=Ljava/lang/annotation/RetentionPolicy;.CLASS ) -## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class - Compiled from "HostSideTestNativeSubstitutionClass.java" -public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation +## Class: android/hosttest/annotation/HostSideTestRedirect.class + Compiled from "HostSideTestRedirect.java" +public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation minor version: 0 major version: 61 flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION - this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass + this_class: #x // android/hosttest/annotation/HostSideTestRedirect + super_class: #x // java/lang/Object + interfaces: 1, fields: 0, methods: 0, attributes: 2 +} +SourceFile: "HostSideTestRedirect.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + x: #x(#x=[e#x.#x]) + java.lang.annotation.Target( + value=[Ljava/lang/annotation/ElementType;.METHOD] + ) + x: #x(#x=e#x.#x) + java.lang.annotation.Retention( + value=Ljava/lang/annotation/RetentionPolicy;.CLASS + ) +## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class + Compiled from "HostSideTestRedirectionClass.java" +public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation + minor version: 0 + major version: 61 + flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION + this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass super_class: #x // java/lang/Object interfaces: 1, fields: 0, methods: 1, attributes: 2 public abstract java.lang.String value(); @@ -64,7 +86,7 @@ public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep } -SourceFile: "HostSideTestNativeSubstitutionClass.java" +SourceFile: "HostSideTestRedirectionClass.java" RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep @@ -2076,7 +2098,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 12, attributes: 3 + interfaces: 0, fields: 1, methods: 14, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -2113,6 +2135,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static int nativeAddTwo_should_be_like_this(int); descriptor: (I)I @@ -2144,6 +2169,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static long nativeLongPlus_should_be_like_this(long, long); descriptor: (JJ)J @@ -2195,6 +2223,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public int nativeNonStaticAddToValue_should_be_like_this(int); descriptor: (I)I @@ -2240,9 +2271,6 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep - RuntimeInvisibleAnnotations: - x: #x() - android.hosttest.annotation.HostSideTestKeep public static void nativeStillNotSupported_should_be_like_this(); descriptor: ()V @@ -2272,6 +2300,42 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public void notNativeRedirected(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=1, locals=1, args_size=1 + x: aload_0 + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + x: return + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=0, locals=0, args_size=0 + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V + x: return + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: @@ -2281,7 +2345,7 @@ RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassKeep x: #x(#x=s#x) - android.hosttest.annotation.HostSideTestNativeSubstitutionClass( + android.hosttest.annotation.HostSideTestRedirectionClass( value="TinyFrameworkNative_host" ) ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class @@ -2292,7 +2356,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 5, attributes: 3 + interfaces: 0, fields: 0, methods: 7, attributes: 3 public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host(); descriptor: ()V flags: (0x0001) ACC_PUBLIC @@ -2381,6 +2445,31 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + + public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative); + descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=0, locals=1, args_size=1 + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 0 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=0, locals=0, args_size=0 + x: return + LineNumberTable: + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep } SourceFile: "TinyFrameworkNative_host.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt index 2ca723bea232..41f459afe78d 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt @@ -67,13 +67,44 @@ RuntimeVisibleAnnotations: java.lang.annotation.Retention( value=Ljava/lang/annotation/RetentionPolicy;.CLASS ) -## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class - Compiled from "HostSideTestNativeSubstitutionClass.java" -public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation +## Class: android/hosttest/annotation/HostSideTestRedirect.class + Compiled from "HostSideTestRedirect.java" +public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation minor version: 0 major version: 61 flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION - this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass + this_class: #x // android/hosttest/annotation/HostSideTestRedirect + super_class: #x // java/lang/Object + interfaces: 1, fields: 0, methods: 1, attributes: 2 + private static {}; + descriptor: ()V + flags: (0x000a) ACC_PRIVATE, ACC_STATIC + Code: + stack=2, locals=0, args_size=0 + x: ldc #x // class android/hosttest/annotation/HostSideTestRedirect + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V + x: return +} +SourceFile: "HostSideTestRedirect.java" +RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + x: #x(#x=[e#x.#x]) + java.lang.annotation.Target( + value=[Ljava/lang/annotation/ElementType;.METHOD] + ) + x: #x(#x=e#x.#x) + java.lang.annotation.Retention( + value=Ljava/lang/annotation/RetentionPolicy;.CLASS + ) +## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class + Compiled from "HostSideTestRedirectionClass.java" +public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation + minor version: 0 + major version: 61 + flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION + this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass super_class: #x // java/lang/Object interfaces: 1, fields: 0, methods: 2, attributes: 2 private static {}; @@ -81,7 +112,7 @@ public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass flags: (0x000a) ACC_PRIVATE, ACC_STATIC Code: stack=2, locals=0, args_size=0 - x: ldc #x // class android/hosttest/annotation/HostSideTestNativeSubstitutionClass + x: ldc #x // class android/hosttest/annotation/HostSideTestRedirectionClass x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V x: return @@ -93,7 +124,7 @@ public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep } -SourceFile: "HostSideTestNativeSubstitutionClass.java" +SourceFile: "HostSideTestRedirectionClass.java" RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep @@ -2612,7 +2643,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative super_class: #x // java/lang/Object - interfaces: 0, fields: 1, methods: 13, attributes: 3 + interfaces: 0, fields: 1, methods: 15, attributes: 3 int value; descriptor: I flags: (0x0000) @@ -2669,6 +2700,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static int nativeAddTwo_should_be_like_this(int); descriptor: (I)I @@ -2710,6 +2744,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public static long nativeLongPlus_should_be_like_this(long, long); descriptor: (JJ)J @@ -2776,6 +2813,9 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect public int nativeNonStaticAddToValue_should_be_like_this(int); descriptor: (I)I @@ -2831,9 +2871,6 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep - RuntimeInvisibleAnnotations: - x: #x() - android.hosttest.annotation.HostSideTestKeep public static void nativeStillNotSupported_should_be_like_this(); descriptor: ()V @@ -2873,6 +2910,52 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public void notNativeRedirected(); + descriptor: ()V + flags: (0x0001) ACC_PUBLIC + Code: + stack=4, locals=1, args_size=1 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative + x: ldc #x // String notNativeRedirected + x: ldc #x // String ()V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: aload_0 + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + x: return + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=0, args_size=0 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative + x: ldc #x // String notNativeStaticRedirected + x: ldc #x // String ()V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V + x: return + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + RuntimeInvisibleAnnotations: + x: #x() + android.hosttest.annotation.HostSideTestRedirect } SourceFile: "TinyFrameworkNative.java" RuntimeVisibleAnnotations: @@ -2882,7 +2965,7 @@ RuntimeInvisibleAnnotations: x: #x() android.hosttest.annotation.HostSideTestWholeClassKeep x: #x(#x=s#x) - android.hosttest.annotation.HostSideTestNativeSubstitutionClass( + android.hosttest.annotation.HostSideTestRedirectionClass( value="TinyFrameworkNative_host" ) ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class @@ -2893,7 +2976,7 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host super_class: #x // java/lang/Object - interfaces: 0, fields: 0, methods: 6, attributes: 3 + interfaces: 0, fields: 0, methods: 8, attributes: 3 private static {}; descriptor: ()V flags: (0x000a) ACC_PRIVATE, ACC_STATIC @@ -3017,6 +3100,41 @@ public class com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host RuntimeVisibleAnnotations: x: #x() com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + + public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative); + descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=1, args_size=1 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host + x: ldc #x // String notNativeRedirected + x: ldc #x // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: return + LineNumberTable: + LocalVariableTable: + Start Length Slot Name Signature + 11 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative; + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep + + public static void notNativeStaticRedirected(); + descriptor: ()V + flags: (0x0009) ACC_PUBLIC, ACC_STATIC + Code: + stack=4, locals=0, args_size=0 + x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host + x: ldc #x // String notNativeStaticRedirected + x: ldc #x // String ()V + x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall + x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + x: return + LineNumberTable: + RuntimeVisibleAnnotations: + x: #x() + com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep } SourceFile: "TinyFrameworkNative_host.java" RuntimeVisibleAnnotations: diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java index 73b5e2fadbad..04a551c8c46e 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java @@ -15,20 +15,23 @@ */ package com.android.hoststubgen.test.tinyframework; -import android.hosttest.annotation.HostSideTestKeep; -import android.hosttest.annotation.HostSideTestNativeSubstitutionClass; +import android.hosttest.annotation.HostSideTestRedirect; +import android.hosttest.annotation.HostSideTestRedirectionClass; import android.hosttest.annotation.HostSideTestThrow; import android.hosttest.annotation.HostSideTestWholeClassKeep; @HostSideTestWholeClassKeep -@HostSideTestNativeSubstitutionClass("TinyFrameworkNative_host") +@HostSideTestRedirectionClass("TinyFrameworkNative_host") public class TinyFrameworkNative { + + @HostSideTestRedirect public static native int nativeAddTwo(int arg); public static int nativeAddTwo_should_be_like_this(int arg) { return TinyFrameworkNative_host.nativeAddTwo(arg); } + @HostSideTestRedirect public static native long nativeLongPlus(long arg1, long arg2); public static long nativeLongPlus_should_be_like_this(long arg1, long arg2) { @@ -41,6 +44,7 @@ public class TinyFrameworkNative { this.value = v; } + @HostSideTestRedirect public native int nativeNonStaticAddToValue(int arg); public int nativeNonStaticAddToValue_should_be_like_this(int arg) { @@ -50,12 +54,22 @@ public class TinyFrameworkNative { @HostSideTestThrow public static native void nativeStillNotSupported(); - @HostSideTestKeep public static native void nativeStillKeep(); public static void nativeStillNotSupported_should_be_like_this() { throw new RuntimeException(); } + @HostSideTestRedirect public static native byte nativeBytePlus(byte arg1, byte arg2); + + @HostSideTestRedirect + public void notNativeRedirected() { + throw new RuntimeException(); + } + + @HostSideTestRedirect + public static void notNativeStaticRedirected() { + throw new RuntimeException(); + } } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java index b23c21602967..c7a29a1cc0f9 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java @@ -17,8 +17,6 @@ package com.android.hoststubgen.test.tinyframework; import android.hosttest.annotation.HostSideTestWholeClassKeep; -// TODO: This annotation shouldn't be needed. -// We should infer it from HostSideTestNativeSubstitutionClass. @HostSideTestWholeClassKeep public class TinyFrameworkNative_host { public static int nativeAddTwo(int arg) { @@ -38,4 +36,10 @@ public class TinyFrameworkNative_host { public static byte nativeBytePlus(byte arg1, byte arg2) { return (byte) (arg1 + arg2); } + + public static void notNativeRedirected(TinyFrameworkNative source) { + } + + public static void notNativeStaticRedirected() { + } } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java index 14229a0ede5f..68673dc2a5b8 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java @@ -164,6 +164,12 @@ public class TinyFrameworkClassTest { } @Test + public void testNotNativeRedirect() { + TinyFrameworkNative.notNativeStaticRedirected(); + new TinyFrameworkNative().notNativeRedirected(); + } + + @Test public void testExitLog() { thrown.expect(RuntimeException.class); thrown.expectMessage("Outer exception"); diff --git a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt index 24d203fd1116..f5af99ec39ac 100644 --- a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt +++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt @@ -24,20 +24,31 @@ import com.intellij.psi.PsiReferenceList import org.jetbrains.uast.UMethod /** - * Given a UMethod, determine if this method is the entrypoint to an interface - * generated by AIDL, returning the interface name if so, otherwise returning - * null + * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL, + * returning the interface name if so, otherwise returning null. */ fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? { + return containingAidlInterfacePsiClass(context, node)?.name +} + +/** + * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL, + * returning the fully qualified interface name if so, otherwise returning null. + */ +fun getContainingAidlInterfaceQualified(context: JavaContext, node: UMethod): String? { + return containingAidlInterfacePsiClass(context, node)?.qualifiedName +} + +private fun containingAidlInterfacePsiClass(context: JavaContext, node: UMethod): PsiClass? { val containingStub = containingStub(context, node) ?: return null val superMethod = node.findSuperMethods(containingStub) if (superMethod.isEmpty()) return null - return containingStub.containingClass?.name + return containingStub.containingClass } -/* Returns the containing Stub class if any. This is not sufficient to infer - * that the method itself extends an AIDL generated method. See - * getContainingAidlInterface for that purpose. +/** + * Returns the containing Stub class if any. This is not sufficient to infer that the method itself + * extends an AIDL generated method. See getContainingAidlInterface for that purpose. */ fun containingStub(context: JavaContext, node: UMethod?): PsiClass? { var superClass = node?.containingClass?.superClass @@ -48,7 +59,7 @@ fun containingStub(context: JavaContext, node: UMethod?): PsiClass? { return null } -private fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean { +fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean { if (psiClass == null) return false if (psiClass.name != "Stub") return false if (!context.evaluator.isStatic(psiClass)) return false diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt new file mode 100644 index 000000000000..8777712b0f04 --- /dev/null +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt @@ -0,0 +1,774 @@ +/* + * Copyright (C) 2024 The Android Open 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.google.android.lint.aidl + +/** + * The exemptAidlInterfaces set was generated by running ExemptAidlInterfacesGenerator on the + * entire source tree. To reproduce the results, run generate-exempt-aidl-interfaces.sh + * located in tools/lint/utils. + * + * TODO: b/363248121 - Use the exemptAidlInterfaces set inside PermissionAnnotationDetector when it + * gets migrated to a global lint check. + */ +val exemptAidlInterfaces = setOf( + "android.accessibilityservice.IAccessibilityServiceConnection", + "android.accessibilityservice.IBrailleDisplayConnection", + "android.accounts.IAccountAuthenticatorResponse", + "android.accounts.IAccountManager", + "android.accounts.IAccountManagerResponse", + "android.adservices.adid.IAdIdProviderService", + "android.adservices.adid.IAdIdService", + "android.adservices.adid.IGetAdIdCallback", + "android.adservices.adid.IGetAdIdProviderCallback", + "android.adservices.adselection.AdSelectionCallback", + "android.adservices.adselection.AdSelectionOverrideCallback", + "android.adservices.adselection.AdSelectionService", + "android.adservices.adselection.GetAdSelectionDataCallback", + "android.adservices.adselection.PersistAdSelectionResultCallback", + "android.adservices.adselection.ReportImpressionCallback", + "android.adservices.adselection.ReportInteractionCallback", + "android.adservices.adselection.SetAppInstallAdvertisersCallback", + "android.adservices.adselection.UpdateAdCounterHistogramCallback", + "android.adservices.appsetid.IAppSetIdProviderService", + "android.adservices.appsetid.IAppSetIdService", + "android.adservices.appsetid.IGetAppSetIdCallback", + "android.adservices.appsetid.IGetAppSetIdProviderCallback", + "android.adservices.cobalt.IAdServicesCobaltUploadService", + "android.adservices.common.IAdServicesCommonCallback", + "android.adservices.common.IAdServicesCommonService", + "android.adservices.common.IAdServicesCommonStatesCallback", + "android.adservices.common.IEnableAdServicesCallback", + "android.adservices.common.IUpdateAdIdCallback", + "android.adservices.customaudience.CustomAudienceOverrideCallback", + "android.adservices.customaudience.FetchAndJoinCustomAudienceCallback", + "android.adservices.customaudience.ICustomAudienceCallback", + "android.adservices.customaudience.ICustomAudienceService", + "android.adservices.customaudience.ScheduleCustomAudienceUpdateCallback", + "android.adservices.extdata.IAdServicesExtDataStorageService", + "android.adservices.extdata.IGetAdServicesExtDataCallback", + "android.adservices.measurement.IMeasurementApiStatusCallback", + "android.adservices.measurement.IMeasurementCallback", + "android.adservices.measurement.IMeasurementService", + "android.adservices.ondevicepersonalization.aidl.IDataAccessService", + "android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback", + "android.adservices.ondevicepersonalization.aidl.IExecuteCallback", + "android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback", + "android.adservices.ondevicepersonalization.aidl.IFederatedComputeService", + "android.adservices.ondevicepersonalization.aidl.IIsolatedModelService", + "android.adservices.ondevicepersonalization.aidl.IIsolatedModelServiceCallback", + "android.adservices.ondevicepersonalization.aidl.IIsolatedService", + "android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback", + "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService", + "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback", + "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationDebugService", + "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService", + "android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback", + "android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback", + "android.adservices.shell.IShellCommand", + "android.adservices.shell.IShellCommandCallback", + "android.adservices.signals.IProtectedSignalsService", + "android.adservices.signals.UpdateSignalsCallback", + "android.adservices.topics.IGetTopicsCallback", + "android.adservices.topics.ITopicsService", + "android.app.admin.IDevicePolicyManager", + "android.app.adservices.IAdServicesManager", + "android.app.ambientcontext.IAmbientContextManager", + "android.app.ambientcontext.IAmbientContextObserver", + "android.app.appsearch.aidl.IAppFunctionService", + "android.app.appsearch.aidl.IAppSearchBatchResultCallback", + "android.app.appsearch.aidl.IAppSearchManager", + "android.app.appsearch.aidl.IAppSearchObserverProxy", + "android.app.appsearch.aidl.IAppSearchResultCallback", + "android.app.backup.IBackupCallback", + "android.app.backup.IBackupManager", + "android.app.backup.IRestoreSession", + "android.app.blob.IBlobCommitCallback", + "android.app.blob.IBlobStoreManager", + "android.app.blob.IBlobStoreSession", + "android.app.contentsuggestions.IContentSuggestionsManager", + "android.app.contextualsearch.IContextualSearchManager", + "android.app.ecm.IEnhancedConfirmationManager", + "android.apphibernation.IAppHibernationService", + "android.app.IActivityClientController", + "android.app.IActivityController", + "android.app.IActivityTaskManager", + "android.app.IAlarmCompleteListener", + "android.app.IAlarmListener", + "android.app.IAlarmManager", + "android.app.IApplicationThread", + "android.app.IAppTask", + "android.app.IAppTraceRetriever", + "android.app.IAssistDataReceiver", + "android.app.IForegroundServiceObserver", + "android.app.IGameManagerService", + "android.app.IGrammaticalInflectionManager", + "android.app.ILocaleManager", + "android.app.INotificationManager", + "android.app.IParcelFileDescriptorRetriever", + "android.app.IProcessObserver", + "android.app.ISearchManager", + "android.app.IStopUserCallback", + "android.app.ITaskStackListener", + "android.app.IUiModeManager", + "android.app.IUriGrantsManager", + "android.app.IUserSwitchObserver", + "android.app.IWallpaperManager", + "android.app.job.IJobCallback", + "android.app.job.IJobScheduler", + "android.app.job.IJobService", + "android.app.ondeviceintelligence.IDownloadCallback", + "android.app.ondeviceintelligence.IFeatureCallback", + "android.app.ondeviceintelligence.IFeatureDetailsCallback", + "android.app.ondeviceintelligence.IListFeaturesCallback", + "android.app.ondeviceintelligence.IOnDeviceIntelligenceManager", + "android.app.ondeviceintelligence.IProcessingSignal", + "android.app.ondeviceintelligence.IResponseCallback", + "android.app.ondeviceintelligence.IStreamingResponseCallback", + "android.app.ondeviceintelligence.ITokenInfoCallback", + "android.app.people.IPeopleManager", + "android.app.pinner.IPinnerService", + "android.app.prediction.IPredictionManager", + "android.app.role.IOnRoleHoldersChangedListener", + "android.app.role.IRoleController", + "android.app.role.IRoleManager", + "android.app.sdksandbox.ILoadSdkCallback", + "android.app.sdksandbox.IRequestSurfacePackageCallback", + "android.app.sdksandbox.ISdkSandboxManager", + "android.app.sdksandbox.ISdkSandboxProcessDeathCallback", + "android.app.sdksandbox.ISdkToServiceCallback", + "android.app.sdksandbox.ISharedPreferencesSyncCallback", + "android.app.sdksandbox.IUnloadSdkCallback", + "android.app.sdksandbox.testutils.testscenario.ISdkSandboxTestExecutor", + "android.app.search.ISearchUiManager", + "android.app.slice.ISliceManager", + "android.app.smartspace.ISmartspaceManager", + "android.app.timedetector.ITimeDetectorService", + "android.app.timezonedetector.ITimeZoneDetectorService", + "android.app.trust.ITrustManager", + "android.app.usage.IStorageStatsManager", + "android.app.usage.IUsageStatsManager", + "android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager", + "android.app.wearable.IWearableSensingCallback", + "android.app.wearable.IWearableSensingManager", + "android.bluetooth.IBluetooth", + "android.bluetooth.IBluetoothA2dp", + "android.bluetooth.IBluetoothA2dpSink", + "android.bluetooth.IBluetoothActivityEnergyInfoListener", + "android.bluetooth.IBluetoothAvrcpController", + "android.bluetooth.IBluetoothCallback", + "android.bluetooth.IBluetoothConnectionCallback", + "android.bluetooth.IBluetoothCsipSetCoordinator", + "android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback", + "android.bluetooth.IBluetoothGatt", + "android.bluetooth.IBluetoothGattCallback", + "android.bluetooth.IBluetoothGattServerCallback", + "android.bluetooth.IBluetoothHapClient", + "android.bluetooth.IBluetoothHapClientCallback", + "android.bluetooth.IBluetoothHeadset", + "android.bluetooth.IBluetoothHeadsetClient", + "android.bluetooth.IBluetoothHearingAid", + "android.bluetooth.IBluetoothHidDevice", + "android.bluetooth.IBluetoothHidDeviceCallback", + "android.bluetooth.IBluetoothHidHost", + "android.bluetooth.IBluetoothLeAudio", + "android.bluetooth.IBluetoothLeAudioCallback", + "android.bluetooth.IBluetoothLeBroadcastAssistant", + "android.bluetooth.IBluetoothLeBroadcastAssistantCallback", + "android.bluetooth.IBluetoothLeBroadcastCallback", + "android.bluetooth.IBluetoothLeCallControl", + "android.bluetooth.IBluetoothLeCallControlCallback", + "android.bluetooth.IBluetoothManager", + "android.bluetooth.IBluetoothManagerCallback", + "android.bluetooth.IBluetoothMap", + "android.bluetooth.IBluetoothMapClient", + "android.bluetooth.IBluetoothMcpServiceManager", + "android.bluetooth.IBluetoothMetadataListener", + "android.bluetooth.IBluetoothOobDataCallback", + "android.bluetooth.IBluetoothPan", + "android.bluetooth.IBluetoothPanCallback", + "android.bluetooth.IBluetoothPbap", + "android.bluetooth.IBluetoothPbapClient", + "android.bluetooth.IBluetoothPreferredAudioProfilesCallback", + "android.bluetooth.IBluetoothQualityReportReadyCallback", + "android.bluetooth.IBluetoothSap", + "android.bluetooth.IBluetoothScan", + "android.bluetooth.IBluetoothSocketManager", + "android.bluetooth.IBluetoothVolumeControl", + "android.bluetooth.IBluetoothVolumeControlCallback", + "android.bluetooth.le.IAdvertisingSetCallback", + "android.bluetooth.le.IDistanceMeasurementCallback", + "android.bluetooth.le.IPeriodicAdvertisingCallback", + "android.bluetooth.le.IScannerCallback", + "android.companion.ICompanionDeviceManager", + "android.companion.IOnMessageReceivedListener", + "android.companion.IOnTransportsChangedListener", + "android.companion.virtualcamera.IVirtualCameraCallback", + "android.companion.virtual.IVirtualDevice", + "android.companion.virtual.IVirtualDeviceManager", + "android.companion.virtualnative.IVirtualDeviceManagerNative", + "android.content.IClipboard", + "android.content.IContentService", + "android.content.IIntentReceiver", + "android.content.IIntentSender", + "android.content.integrity.IAppIntegrityManager", + "android.content.IRestrictionsManager", + "android.content.ISyncAdapterUnsyncableAccountCallback", + "android.content.ISyncContext", + "android.content.om.IOverlayManager", + "android.content.pm.dex.IArtManager", + "android.content.pm.dex.ISnapshotRuntimeProfileCallback", + "android.content.pm.IBackgroundInstallControlService", + "android.content.pm.ICrossProfileApps", + "android.content.pm.IDataLoaderManager", + "android.content.pm.IDataLoaderStatusListener", + "android.content.pm.ILauncherApps", + "android.content.pm.IOnChecksumsReadyListener", + "android.content.pm.IOtaDexopt", + "android.content.pm.IPackageDataObserver", + "android.content.pm.IPackageDeleteObserver", + "android.content.pm.IPackageInstaller", + "android.content.pm.IPackageInstallerSession", + "android.content.pm.IPackageInstallerSessionFileSystemConnector", + "android.content.pm.IPackageInstallObserver2", + "android.content.pm.IPackageLoadingProgressCallback", + "android.content.pm.IPackageManager", + "android.content.pm.IPackageManagerNative", + "android.content.pm.IPackageMoveObserver", + "android.content.pm.IPinItemRequest", + "android.content.pm.IShortcutService", + "android.content.pm.IStagedApexObserver", + "android.content.pm.verify.domain.IDomainVerificationManager", + "android.content.res.IResourcesManager", + "android.content.rollback.IRollbackManager", + "android.credentials.ICredentialManager", + "android.debug.IAdbTransport", + "android.devicelock.IDeviceLockService", + "android.devicelock.IGetDeviceIdCallback", + "android.devicelock.IGetKioskAppsCallback", + "android.devicelock.IIsDeviceLockedCallback", + "android.devicelock.IVoidResultCallback", + "android.federatedcompute.aidl.IExampleStoreCallback", + "android.federatedcompute.aidl.IExampleStoreIterator", + "android.federatedcompute.aidl.IExampleStoreIteratorCallback", + "android.federatedcompute.aidl.IExampleStoreService", + "android.federatedcompute.aidl.IFederatedComputeCallback", + "android.federatedcompute.aidl.IFederatedComputeService", + "android.federatedcompute.aidl.IResultHandlingService", + "android.flags.IFeatureFlags", + "android.frameworks.location.altitude.IAltitudeService", + "android.frameworks.vibrator.IVibratorController", + "android.frameworks.vibrator.IVibratorControlService", + "android.gsi.IGsiServiceCallback", + "android.hardware.biometrics.AuthenticationStateListener", + "android.hardware.biometrics.common.ICancellationSignal", + "android.hardware.biometrics.face.IFace", + "android.hardware.biometrics.face.ISession", + "android.hardware.biometrics.face.ISessionCallback", + "android.hardware.biometrics.fingerprint.IFingerprint", + "android.hardware.biometrics.fingerprint.ISession", + "android.hardware.biometrics.fingerprint.ISessionCallback", + "android.hardware.biometrics.IAuthService", + "android.hardware.biometrics.IBiometricAuthenticator", + "android.hardware.biometrics.IBiometricContextListener", + "android.hardware.biometrics.IBiometricSensorReceiver", + "android.hardware.biometrics.IBiometricService", + "android.hardware.biometrics.IBiometricStateListener", + "android.hardware.biometrics.IBiometricSysuiReceiver", + "android.hardware.biometrics.IInvalidationCallback", + "android.hardware.biometrics.ITestSession", + "android.hardware.broadcastradio.IAnnouncementListener", + "android.hardware.broadcastradio.ITunerCallback", + "android.hardware.contexthub.IContextHubCallback", + "android.hardware.devicestate.IDeviceStateManager", + "android.hardware.display.IColorDisplayManager", + "android.hardware.display.IDisplayManager", + "android.hardware.face.IFaceAuthenticatorsRegisteredCallback", + "android.hardware.face.IFaceService", + "android.hardware.face.IFaceServiceReceiver", + "android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback", + "android.hardware.fingerprint.IFingerprintClientActiveCallback", + "android.hardware.fingerprint.IFingerprintService", + "android.hardware.fingerprint.IFingerprintServiceReceiver", + "android.hardware.fingerprint.IUdfpsOverlayControllerCallback", + "android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback", + "android.hardware.hdmi.IHdmiControlCallback", + "android.hardware.hdmi.IHdmiControlService", + "android.hardware.hdmi.IHdmiDeviceEventListener", + "android.hardware.hdmi.IHdmiHotplugEventListener", + "android.hardware.hdmi.IHdmiSystemAudioModeChangeListener", + "android.hardware.health.IHealthInfoCallback", + "android.hardware.ICameraServiceProxy", + "android.hardware.IConsumerIrService", + "android.hardware.input.IInputManager", + "android.hardware.iris.IIrisService", + "android.hardware.ISensorPrivacyManager", + "android.hardware.ISerialManager", + "android.hardware.lights.ILightsManager", + "android.hardware.location.IContextHubClient", + "android.hardware.location.IContextHubClientCallback", + "android.hardware.location.IContextHubService", + "android.hardware.location.IContextHubTransactionCallback", + "android.hardware.location.ISignificantPlaceProviderManager", + "android.hardware.radio.IAnnouncementListener", + "android.hardware.radio.ICloseHandle", + "android.hardware.radio.ims.media.IImsMedia", + "android.hardware.radio.ims.media.IImsMediaListener", + "android.hardware.radio.ims.media.IImsMediaSession", + "android.hardware.radio.ims.media.IImsMediaSessionListener", + "android.hardware.radio.IRadioService", + "android.hardware.radio.ITuner", + "android.hardware.radio.sap.ISapCallback", + "android.hardware.soundtrigger3.ISoundTriggerHw", + "android.hardware.soundtrigger3.ISoundTriggerHwCallback", + "android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback", + "android.hardware.soundtrigger.IRecognitionStatusCallback", + "android.hardware.tetheroffload.ITetheringOffloadCallback", + "android.hardware.thermal.IThermalChangedCallback", + "android.hardware.tv.hdmi.cec.IHdmiCecCallback", + "android.hardware.tv.hdmi.connection.IHdmiConnectionCallback", + "android.hardware.tv.hdmi.earc.IEArcCallback", + "android.hardware.usb.gadget.IUsbGadgetCallback", + "android.hardware.usb.IUsbCallback", + "android.hardware.usb.IUsbManager", + "android.hardware.usb.IUsbSerialReader", + "android.hardware.wifi.hostapd.IHostapdCallback", + "android.hardware.wifi.IWifiChipEventCallback", + "android.hardware.wifi.IWifiEventCallback", + "android.hardware.wifi.IWifiNanIfaceEventCallback", + "android.hardware.wifi.IWifiRttControllerEventCallback", + "android.hardware.wifi.IWifiStaIfaceEventCallback", + "android.hardware.wifi.supplicant.INonStandardCertCallback", + "android.hardware.wifi.supplicant.ISupplicantP2pIfaceCallback", + "android.hardware.wifi.supplicant.ISupplicantStaIfaceCallback", + "android.hardware.wifi.supplicant.ISupplicantStaNetworkCallback", + "android.health.connect.aidl.IAccessLogsResponseCallback", + "android.health.connect.aidl.IActivityDatesResponseCallback", + "android.health.connect.aidl.IAggregateRecordsResponseCallback", + "android.health.connect.aidl.IApplicationInfoResponseCallback", + "android.health.connect.aidl.IChangeLogsResponseCallback", + "android.health.connect.aidl.IDataStagingFinishedCallback", + "android.health.connect.aidl.IEmptyResponseCallback", + "android.health.connect.aidl.IGetChangeLogTokenCallback", + "android.health.connect.aidl.IGetHealthConnectDataStateCallback", + "android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback", + "android.health.connect.aidl.IGetPriorityResponseCallback", + "android.health.connect.aidl.IHealthConnectService", + "android.health.connect.aidl.IInsertRecordsResponseCallback", + "android.health.connect.aidl.IMedicalDataSourceResponseCallback", + "android.health.connect.aidl.IMedicalResourcesResponseCallback", + "android.health.connect.aidl.IMigrationCallback", + "android.health.connect.aidl.IReadMedicalResourcesResponseCallback", + "android.health.connect.aidl.IReadRecordsResponseCallback", + "android.health.connect.aidl.IRecordTypeInfoResponseCallback", + "android.health.connect.exportimport.IImportStatusCallback", + "android.health.connect.exportimport.IQueryDocumentProvidersCallback", + "android.health.connect.exportimport.IScheduledExportStatusCallback", + "android.location.ICountryDetector", + "android.location.IGpsGeofenceHardware", + "android.location.ILocationManager", + "android.location.provider.ILocationProviderManager", + "android.media.IAudioRoutesObserver", + "android.media.IMediaCommunicationService", + "android.media.IMediaCommunicationServiceCallback", + "android.media.IMediaController2", + "android.media.IMediaRoute2ProviderServiceCallback", + "android.media.IMediaRouterService", + "android.media.IMediaSession2", + "android.media.IMediaSession2Service", + "android.media.INativeSpatializerCallback", + "android.media.IPlaybackConfigDispatcher", + "android.media.IRecordingConfigDispatcher", + "android.media.IRemoteDisplayCallback", + "android.media.ISoundDoseCallback", + "android.media.ISpatializerHeadTrackingCallback", + "android.media.ITranscodingClientCallback", + "android.media.metrics.IMediaMetricsManager", + "android.media.midi.IMidiManager", + "android.media.musicrecognition.IMusicRecognitionAttributionTagCallback", + "android.media.musicrecognition.IMusicRecognitionManager", + "android.media.musicrecognition.IMusicRecognitionServiceCallback", + "android.media.projection.IMediaProjection", + "android.media.projection.IMediaProjectionCallback", + "android.media.projection.IMediaProjectionManager", + "android.media.projection.IMediaProjectionWatcherCallback", + "android.media.session.ISession", + "android.media.session.ISessionController", + "android.media.session.ISessionManager", + "android.media.soundtrigger.ISoundTriggerDetectionServiceClient", + "android.media.soundtrigger_middleware.IInjectGlobalEvent", + "android.media.soundtrigger_middleware.IInjectModelEvent", + "android.media.soundtrigger_middleware.IInjectRecognitionEvent", + "android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService", + "android.media.soundtrigger_middleware.ISoundTriggerModule", + "android.media.tv.ad.ITvAdManager", + "android.media.tv.ad.ITvAdSessionCallback", + "android.media.tv.interactive.ITvInteractiveAppManager", + "android.media.tv.interactive.ITvInteractiveAppServiceCallback", + "android.media.tv.interactive.ITvInteractiveAppSessionCallback", + "android.media.tv.ITvInputHardware", + "android.media.tv.ITvInputManager", + "android.media.tv.ITvInputServiceCallback", + "android.media.tv.ITvInputSessionCallback", + "android.media.tv.ITvRemoteServiceInput", + "android.nearby.aidl.IOffloadCallback", + "android.nearby.IBroadcastListener", + "android.nearby.INearbyManager", + "android.nearby.IScanListener", + "android.net.connectivity.aidl.ConnectivityNative", + "android.net.dhcp.IDhcpEventCallbacks", + "android.net.dhcp.IDhcpServer", + "android.net.dhcp.IDhcpServerCallbacks", + "android.net.ICaptivePortal", + "android.net.IConnectivityDiagnosticsCallback", + "android.net.IConnectivityManager", + "android.net.IEthernetManager", + "android.net.IEthernetServiceListener", + "android.net.IIntResultListener", + "android.net.IIpConnectivityMetrics", + "android.net.IIpMemoryStore", + "android.net.IIpMemoryStoreCallbacks", + "android.net.IIpSecService", + "android.net.INetdEventCallback", + "android.net.INetdUnsolicitedEventListener", + "android.net.INetworkActivityListener", + "android.net.INetworkAgent", + "android.net.INetworkAgentRegistry", + "android.net.INetworkInterfaceOutcomeReceiver", + "android.net.INetworkManagementEventObserver", + "android.net.INetworkMonitor", + "android.net.INetworkMonitorCallbacks", + "android.net.INetworkOfferCallback", + "android.net.INetworkPolicyListener", + "android.net.INetworkPolicyManager", + "android.net.INetworkScoreService", + "android.net.INetworkStackConnector", + "android.net.INetworkStackStatusCallback", + "android.net.INetworkStatsService", + "android.net.INetworkStatsSession", + "android.net.IOnCompleteListener", + "android.net.IPacProxyManager", + "android.net.ip.IIpClient", + "android.net.ip.IIpClientCallbacks", + "android.net.ipmemorystore.IOnBlobRetrievedListener", + "android.net.ipmemorystore.IOnL2KeyResponseListener", + "android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener", + "android.net.ipmemorystore.IOnSameL3NetworkResponseListener", + "android.net.ipmemorystore.IOnStatusAndCountListener", + "android.net.ipmemorystore.IOnStatusListener", + "android.net.IQosCallback", + "android.net.ISocketKeepaliveCallback", + "android.net.ITestNetworkManager", + "android.net.ITetheredInterfaceCallback", + "android.net.ITetheringConnector", + "android.net.ITetheringEventCallback", + "android.net.IVpnManager", + "android.net.mdns.aidl.IMDnsEventListener", + "android.net.metrics.INetdEventListener", + "android.net.netstats.IUsageCallback", + "android.net.netstats.provider.INetworkStatsProvider", + "android.net.netstats.provider.INetworkStatsProviderCallback", + "android.net.nsd.INsdManager", + "android.net.nsd.INsdManagerCallback", + "android.net.nsd.INsdServiceConnector", + "android.net.nsd.IOffloadEngine", + "android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener", + "android.net.thread.IActiveOperationalDatasetReceiver", + "android.net.thread.IConfigurationReceiver", + "android.net.thread.IOperationalDatasetCallback", + "android.net.thread.IOperationReceiver", + "android.net.thread.IStateCallback", + "android.net.thread.IThreadNetworkController", + "android.net.thread.IThreadNetworkManager", + "android.net.vcn.IVcnManagementService", + "android.net.wear.ICompanionDeviceManagerProxy", + "android.net.wifi.aware.IWifiAwareDiscoverySessionCallback", + "android.net.wifi.aware.IWifiAwareEventCallback", + "android.net.wifi.aware.IWifiAwareMacAddressProvider", + "android.net.wifi.aware.IWifiAwareManager", + "android.net.wifi.hotspot2.IProvisioningCallback", + "android.net.wifi.IActionListener", + "android.net.wifi.IBooleanListener", + "android.net.wifi.IByteArrayListener", + "android.net.wifi.ICoexCallback", + "android.net.wifi.IDppCallback", + "android.net.wifi.IIntegerListener", + "android.net.wifi.IInterfaceCreationInfoCallback", + "android.net.wifi.ILastCallerListener", + "android.net.wifi.IListListener", + "android.net.wifi.ILocalOnlyConnectionStatusListener", + "android.net.wifi.ILocalOnlyHotspotCallback", + "android.net.wifi.IMacAddressListListener", + "android.net.wifi.IMapListener", + "android.net.wifi.INetworkRequestMatchCallback", + "android.net.wifi.INetworkRequestUserSelectionCallback", + "android.net.wifi.IOnWifiActivityEnergyInfoListener", + "android.net.wifi.IOnWifiDriverCountryCodeChangedListener", + "android.net.wifi.IOnWifiUsabilityStatsListener", + "android.net.wifi.IPnoScanResultsCallback", + "android.net.wifi.IScanDataListener", + "android.net.wifi.IScanResultsCallback", + "android.net.wifi.IScoreUpdateObserver", + "android.net.wifi.ISoftApCallback", + "android.net.wifi.IStringListener", + "android.net.wifi.ISubsystemRestartCallback", + "android.net.wifi.ISuggestionConnectionStatusListener", + "android.net.wifi.ISuggestionUserApprovalStatusListener", + "android.net.wifi.ITrafficStateCallback", + "android.net.wifi.ITwtCallback", + "android.net.wifi.ITwtCapabilitiesListener", + "android.net.wifi.ITwtStatsListener", + "android.net.wifi.IWifiBandsListener", + "android.net.wifi.IWifiConnectedNetworkScorer", + "android.net.wifi.IWifiLowLatencyLockListener", + "android.net.wifi.IWifiManager", + "android.net.wifi.IWifiNetworkSelectionConfigListener", + "android.net.wifi.IWifiNetworkStateChangedListener", + "android.net.wifi.IWifiScanner", + "android.net.wifi.IWifiScannerListener", + "android.net.wifi.IWifiVerboseLoggingStatusChangedListener", + "android.net.wifi.p2p.IWifiP2pListener", + "android.net.wifi.p2p.IWifiP2pManager", + "android.net.wifi.rtt.IRttCallback", + "android.net.wifi.rtt.IWifiRttManager", + "android.ondevicepersonalization.IOnDevicePersonalizationSystemService", + "android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback", + "android.os.IBatteryPropertiesRegistrar", + "android.os.ICancellationSignal", + "android.os.IDeviceIdentifiersPolicyService", + "android.os.IDeviceIdleController", + "android.os.IDumpstate", + "android.os.IDumpstateListener", + "android.os.IExternalVibratorService", + "android.os.IHardwarePropertiesManager", + "android.os.IHintManager", + "android.os.IHintSession", + "android.os.IIncidentCompanion", + "android.os.image.IDynamicSystemService", + "android.os.incremental.IStorageHealthListener", + "android.os.INetworkManagementService", + "android.os.IPendingIntentRef", + "android.os.IPowerStatsService", + "android.os.IProfilingResultCallback", + "android.os.IProfilingService", + "android.os.IProgressListener", + "android.os.IPullAtomCallback", + "android.os.IRecoverySystem", + "android.os.IRemoteCallback", + "android.os.ISecurityStateManager", + "android.os.IServiceCallback", + "android.os.IStatsCompanionService", + "android.os.IStatsManagerService", + "android.os.IStatsQueryCallback", + "android.os.ISystemConfig", + "android.os.ISystemUpdateManager", + "android.os.IThermalEventListener", + "android.os.IUpdateLock", + "android.os.IUserManager", + "android.os.IUserRestrictionsListener", + "android.os.IVibratorManagerService", + "android.os.IVoldListener", + "android.os.IVoldMountCallback", + "android.os.IVoldTaskListener", + "android.os.logcat.ILogcatManagerService", + "android.permission.ILegacyPermissionManager", + "android.permission.IPermissionChecker", + "android.permission.IPermissionManager", + "android.print.IPrintManager", + "android.print.IPrintSpoolerCallbacks", + "android.print.IPrintSpoolerClient", + "android.printservice.IPrintServiceClient", + "android.printservice.recommendation.IRecommendationServiceCallbacks", + "android.provider.aidl.IDeviceConfigManager", + "android.remoteauth.IDeviceDiscoveryListener", + "android.safetycenter.IOnSafetyCenterDataChangedListener", + "android.safetycenter.ISafetyCenterManager", + "android.scheduling.IRebootReadinessManager", + "android.scheduling.IRequestRebootReadinessStatusListener", + "android.security.attestationverification.IAttestationVerificationManagerService", + "android.security.IFileIntegrityService", + "android.security.keystore.IKeyAttestationApplicationIdProvider", + "android.security.rkp.IRegistration", + "android.security.rkp.IRemoteProvisioning", + "android.service.appprediction.IPredictionService", + "android.service.assist.classification.IFieldClassificationCallback", + "android.service.attention.IAttentionCallback", + "android.service.attention.IProximityUpdateCallback", + "android.service.autofill.augmented.IFillCallback", + "android.service.autofill.IConvertCredentialCallback", + "android.service.autofill.IFillCallback", + "android.service.autofill.IInlineSuggestionUiCallback", + "android.service.autofill.ISaveCallback", + "android.service.autofill.ISurfacePackageResultCallback", + "android.service.contentcapture.IContentCaptureServiceCallback", + "android.service.contentcapture.IContentProtectionAllowlistCallback", + "android.service.contentcapture.IDataShareCallback", + "android.service.credentials.IBeginCreateCredentialCallback", + "android.service.credentials.IBeginGetCredentialCallback", + "android.service.credentials.IClearCredentialStateCallback", + "android.service.dreams.IDreamManager", + "android.service.games.IGameServiceController", + "android.service.games.IGameSessionController", + "android.service.notification.IStatusBarNotificationHolder", + "android.service.oemlock.IOemLockService", + "android.service.ondeviceintelligence.IProcessingUpdateStatusCallback", + "android.service.ondeviceintelligence.IRemoteProcessingService", + "android.service.ondeviceintelligence.IRemoteStorageService", + "android.service.persistentdata.IPersistentDataBlockService", + "android.service.resolver.IResolverRankerResult", + "android.service.rotationresolver.IRotationResolverCallback", + "android.service.textclassifier.ITextClassifierCallback", + "android.service.textclassifier.ITextClassifierService", + "android.service.timezone.ITimeZoneProviderManager", + "android.service.trust.ITrustAgentServiceCallback", + "android.service.voice.IDetectorSessionStorageService", + "android.service.voice.IDetectorSessionVisualQueryDetectionCallback", + "android.service.voice.IDspHotwordDetectionCallback", + "android.service.wallpaper.IWallpaperConnection", + "android.speech.IRecognitionListener", + "android.speech.IRecognitionService", + "android.speech.IRecognitionServiceManager", + "android.speech.tts.ITextToSpeechManager", + "android.speech.tts.ITextToSpeechSession", + "android.system.composd.ICompilationTaskCallback", + "android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback", + "android.system.virtualizationservice.IVirtualMachineCallback", + "android.system.vmtethering.IVmTethering", + "android.telephony.imsmedia.IImsAudioSession", + "android.telephony.imsmedia.IImsAudioSessionCallback", + "android.telephony.imsmedia.IImsMedia", + "android.telephony.imsmedia.IImsMediaCallback", + "android.telephony.imsmedia.IImsTextSession", + "android.telephony.imsmedia.IImsTextSessionCallback", + "android.telephony.imsmedia.IImsVideoSession", + "android.telephony.imsmedia.IImsVideoSessionCallback", + "android.tracing.ITracingServiceProxy", + "android.uwb.IOnUwbActivityEnergyInfoListener", + "android.uwb.IUwbAdapter", + "android.uwb.IUwbAdapterStateCallbacks", + "android.uwb.IUwbAdfProvisionStateCallbacks", + "android.uwb.IUwbOemExtensionCallback", + "android.uwb.IUwbRangingCallbacks", + "android.uwb.IUwbVendorUciCallback", + "android.view.accessibility.IAccessibilityInteractionConnectionCallback", + "android.view.accessibility.IAccessibilityManager", + "android.view.accessibility.IMagnificationConnectionCallback", + "android.view.accessibility.IRemoteMagnificationAnimationCallback", + "android.view.autofill.IAutoFillManager", + "android.view.autofill.IAutofillWindowPresenter", + "android.view.contentcapture.IContentCaptureManager", + "android.view.IDisplayChangeWindowCallback", + "android.view.IDisplayWindowListener", + "android.view.IInputFilter", + "android.view.IInputFilterHost", + "android.view.IInputMonitorHost", + "android.view.IRecentsAnimationController", + "android.view.IRemoteAnimationFinishedCallback", + "android.view.ISensitiveContentProtectionManager", + "android.view.IWindowId", + "android.view.IWindowManager", + "android.view.IWindowSession", + "android.view.translation.ITranslationManager", + "android.view.translation.ITranslationServiceCallback", + "android.webkit.IWebViewUpdateService", + "android.window.IBackAnimationFinishedCallback", + "android.window.IDisplayAreaOrganizerController", + "android.window.ITaskFragmentOrganizerController", + "android.window.ITaskOrganizerController", + "android.window.ITransitionMetricsReporter", + "android.window.IUnhandledDragCallback", + "android.window.IWindowContainerToken", + "android.window.IWindowlessStartingSurfaceCallback", + "android.window.IWindowOrganizerController", + "androidx.core.uwb.backend.IUwb", + "androidx.core.uwb.backend.IUwbClient", + "com.android.clockwork.modes.IModeManager", + "com.android.clockwork.modes.IStateChangeListener", + "com.android.clockwork.power.IWearPowerService", + "com.android.devicelockcontroller.IDeviceLockControllerService", + "com.android.devicelockcontroller.storage.IGlobalParametersService", + "com.android.devicelockcontroller.storage.ISetupParametersService", + "com.android.federatedcompute.services.training.aidl.IIsolatedTrainingService", + "com.android.federatedcompute.services.training.aidl.ITrainingResultCallback", + "com.android.internal.app.IAppOpsActiveCallback", + "com.android.internal.app.ILogAccessDialogCallback", + "com.android.internal.app.ISoundTriggerService", + "com.android.internal.app.ISoundTriggerSession", + "com.android.internal.app.IVoiceInteractionAccessibilitySettingsListener", + "com.android.internal.app.IVoiceInteractionManagerService", + "com.android.internal.app.IVoiceInteractionSessionListener", + "com.android.internal.app.IVoiceInteractionSessionShowCallback", + "com.android.internal.app.IVoiceInteractionSoundTriggerSession", + "com.android.internal.app.procstats.IProcessStats", + "com.android.internal.appwidget.IAppWidgetService", + "com.android.internal.backup.ITransportStatusCallback", + "com.android.internal.compat.IOverrideValidator", + "com.android.internal.compat.IPlatformCompat", + "com.android.internal.compat.IPlatformCompatNative", + "com.android.internal.graphics.fonts.IFontManager", + "com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback", + "com.android.internal.inputmethod.IConnectionlessHandwritingCallback", + "com.android.internal.inputmethod.IImeTracker", + "com.android.internal.inputmethod.IInlineSuggestionsRequestCallback", + "com.android.internal.inputmethod.IInputContentUriToken", + "com.android.internal.inputmethod.IInputMethodPrivilegedOperations", + "com.android.internal.inputmethod.IInputMethodSessionCallback", + "com.android.internal.net.INetworkWatchlistManager", + "com.android.internal.os.IBinaryTransparencyService", + "com.android.internal.os.IDropBoxManagerService", + "com.android.internal.policy.IKeyguardDismissCallback", + "com.android.internal.policy.IKeyguardDrawnCallback", + "com.android.internal.policy.IKeyguardExitCallback", + "com.android.internal.policy.IKeyguardStateCallback", + "com.android.internal.statusbar.IAddTileResultCallback", + "com.android.internal.statusbar.ISessionListener", + "com.android.internal.statusbar.IStatusBarService", + "com.android.internal.telecom.IDeviceIdleControllerAdapter", + "com.android.internal.telecom.IInternalServiceRetriever", + "com.android.internal.telephony.IMms", + "com.android.internal.telephony.ITelephonyRegistry", + "com.android.internal.textservice.ISpellCheckerServiceCallback", + "com.android.internal.textservice.ITextServicesManager", + "com.android.internal.view.IDragAndDropPermissions", + "com.android.internal.view.IInputMethodManager", + "com.android.internal.view.inline.IInlineContentProvider", + "com.android.internal.widget.ILockSettings", + "com.android.net.IProxyPortListener", + "com.android.net.module.util.IRoutingCoordinator", + "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginCallback", + "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginExecutorService", + "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginStateCallback", + "com.android.rkpdapp.IGetKeyCallback", + "com.android.rkpdapp.IGetRegistrationCallback", + "com.android.rkpdapp.IRegistration", + "com.android.rkpdapp.IRemoteProvisioning", + "com.android.rkpdapp.IStoreUpgradedKeyCallback", + "com.android.sdksandbox.IComputeSdkStorageCallback", + "com.android.sdksandbox.ILoadSdkInSandboxCallback", + "com.android.sdksandbox.IRequestSurfacePackageFromSdkCallback", + "com.android.sdksandbox.ISdkSandboxManagerToSdkSandboxCallback", + "com.android.sdksandbox.ISdkSandboxService", + "com.android.sdksandbox.IUnloadSdkInSandboxCallback", + "com.android.server.profcollect.IProviderStatusCallback", + "com.android.server.thread.openthread.IChannelMasksReceiver", + "com.android.server.thread.openthread.INsdPublisher", + "com.android.server.thread.openthread.IOtDaemonCallback", + "com.android.server.thread.openthread.IOtStatusReceiver", + "com.google.android.clockwork.ambient.offload.IDisplayOffloadService", + "com.google.android.clockwork.ambient.offload.IDisplayOffloadTransitionFinishedCallbacks", + "com.google.android.clockwork.healthservices.IHealthService", + "vendor.google_clockwork.healthservices.IHealthServicesCallback", +) diff --git a/tools/lint/utils/README.md b/tools/lint/utils/README.md new file mode 100644 index 000000000000..b5583c54b25c --- /dev/null +++ b/tools/lint/utils/README.md @@ -0,0 +1,11 @@ +# Utility Android Lint Checks for AOSP + +This directory contains scripts that execute utility Android Lint Checks for AOSP, specifically: +* `enforce_permission_counter.py`: Provides statistics regarding the percentage of annotated/not + annotated `AIDL` methods with `@EnforcePermission` annotations. +* `generate-exempt-aidl-interfaces.sh`: Provides a list of all `AIDL` interfaces in the entire + source tree. + +When adding a new utility Android Lint check to this directory, consider adding any utility or +data processing tool you might require. Make sure that your contribution is documented in this +README file. diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt index fa61c42ef8e6..98428810c0fc 100644 --- a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt +++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt @@ -19,6 +19,7 @@ package com.google.android.lint import com.android.tools.lint.client.api.IssueRegistry import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API +import com.google.android.lint.aidl.ExemptAidlInterfacesGenerator import com.google.android.lint.aidl.AnnotatedAidlCounter import com.google.auto.service.AutoService @@ -27,6 +28,7 @@ import com.google.auto.service.AutoService class AndroidUtilsIssueRegistry : IssueRegistry() { override val issues = listOf( AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER, + ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES, ) override val api: Int @@ -38,6 +40,6 @@ class AndroidUtilsIssueRegistry : IssueRegistry() { override val vendor: Vendor = Vendor( vendorName = "Android", feedbackUrl = "http://b/issues/new?component=315013", - contact = "tweek@google.com" + contact = "android-platform-abuse-prevention-withfriends@google.com" ) } diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt new file mode 100644 index 000000000000..6ad223c87a29 --- /dev/null +++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open 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.google.android.lint.aidl + +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Context +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 org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UMethod + +/** + * Generates a set of fully qualified AIDL Interface names present in the entire source tree with + * the following requirement: their implementations have to be inside directories whose path + * prefixes match `systemServicePathPrefixes`. + */ +class ExemptAidlInterfacesGenerator : AidlImplementationDetector() { + private val targetExemptAidlInterfaceNames = mutableSetOf<String>() + private val systemServicePathPrefixes = setOf( + "frameworks/base/services", + "frameworks/base/apex", + "frameworks/opt/wear", + "packages/modules" + ) + + // We could've improved performance by visiting classes rather than methods, however, this lint + // check won't be run regularly, hence we've decided not to add extra overrides to + // AidlImplementationDetector. + override fun visitAidlMethod( + context: JavaContext, + node: UMethod, + interfaceName: String, + body: UBlockExpression + ) { + val filePath = context.file.path + + // We perform `filePath.contains` instead of `filePath.startsWith` since getting the + // relative path of a source file is non-trivial. That is because `context.file.path` + // returns the path to where soong builds the file (i.e. /out/soong/...). Moreover, the + // logic to extract the relative path would need to consider several /out/soong/... + // locations patterns. + if (systemServicePathPrefixes.none { filePath.contains(it) }) return + + val fullyQualifiedInterfaceName = + getContainingAidlInterfaceQualified(context, node) ?: return + + targetExemptAidlInterfaceNames.add("\"$fullyQualifiedInterfaceName\",") + } + + override fun afterCheckEachProject(context: Context) { + if (targetExemptAidlInterfaceNames.isEmpty()) return + + val message = targetExemptAidlInterfaceNames.joinToString("\n") + + context.report( + ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES, + context.getLocation(context.project.dir), + "\n" + message + "\n", + ) + } + + companion object { + @JvmField + val ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES = Issue.create( + id = "PermissionAnnotationExemptAidlInterfaces", + briefDescription = "Returns a set of all AIDL interfaces", + explanation = """ + Produces the exemptAidlInterfaces set used by PermissionAnnotationDetector + """.trimIndent(), + category = Category.SECURITY, + priority = 5, + severity = Severity.INFORMATIONAL, + implementation = Implementation( + ExemptAidlInterfacesGenerator::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt new file mode 100644 index 000000000000..9a17bb4c8d3e --- /dev/null +++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 The Android Open 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.google.android.lint.aidl + +import com.android.tools.lint.checks.infrastructure.LintDetectorTest +import com.android.tools.lint.checks.infrastructure.TestFile +import com.android.tools.lint.checks.infrastructure.TestLintTask +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Issue + +class ExemptAidlInterfacesGeneratorTest : LintDetectorTest() { + override fun getDetector(): Detector = ExemptAidlInterfacesGenerator() + + override fun getIssues(): List<Issue> = listOf( + ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES, + ) + + override fun lint(): TestLintTask = super.lint().allowMissingSdk(true) + + fun testMultipleAidlInterfacesImplemented() { + lint() + .files( + java( + createVisitedPath("TestClass1.java"), + """ + package com.android.server; + public class TestClass1 extends IFoo.Stub { + public void testMethod() {} + } + """ + ) + .indented(), + java( + createVisitedPath("TestClass2.java"), + """ + package com.android.server; + public class TestClass2 extends IBar.Stub { + public void testMethod() {} + } + """ + ) + .indented(), + *stubs, + ) + .run() + .expect( + """ + app: Information: "IFoo", + "IBar", [PermissionAnnotationExemptAidlInterfaces] + 0 errors, 0 warnings + """ + ) + } + + fun testSingleAidlInterfaceRepeated() { + lint() + .files( + java( + createVisitedPath("TestClass1.java"), + """ + package com.android.server; + public class TestClass1 extends IFoo.Stub { + public void testMethod() {} + } + """ + ) + .indented(), + java( + createVisitedPath("TestClass2.java"), + """ + package com.android.server; + public class TestClass2 extends IFoo.Stub { + public void testMethod() {} + } + """ + ) + .indented(), + *stubs, + ) + .run() + .expect( + """ + app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces] + 0 errors, 0 warnings + """ + ) + } + + fun testAnonymousClassExtendsAidlStub() { + lint() + .files( + java( + createVisitedPath("TestClass.java"), + """ + package com.android.server; + public class TestClass { + private IBinder aidlImpl = new IFoo.Stub() { + public void testMethod() {} + }; + } + """ + ) + .indented(), + *stubs, + ) + .run() + .expect( + """ + app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces] + 0 errors, 0 warnings + """ + ) + } + + fun testNoAidlInterfacesImplemented() { + lint() + .files( + java( + createVisitedPath("TestClass.java"), + """ + package com.android.server; + public class TestClass { + public void testMethod() {} + } + """ + ) + .indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAidlInterfaceImplementedInIgnoredDirectory() { + lint() + .files( + java( + ignoredPath, + """ + package com.android.server; + public class TestClass1 extends IFoo.Stub { + public void testMethod() {} + } + """ + ) + .indented(), + *stubs, + ) + .run() + .expectClean() + } + + private val interfaceIFoo: TestFile = java( + """ + public interface IFoo extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IFoo {} + public void testMethod(); + } + """ + ).indented() + + private val interfaceIBar: TestFile = java( + """ + public interface IBar extends android.os.IInterface { + public static abstract class Stub extends android.os.Binder implements IBar {} + public void testMethod(); + } + """ + ).indented() + + private val stubs = arrayOf(interfaceIFoo, interfaceIBar) + + private fun createVisitedPath(filename: String) = + "src/frameworks/base/services/java/com/android/server/$filename" + + private val ignoredPath = "src/test/pkg/TestClass.java" +} diff --git a/tools/lint/utils/generate-exempt-aidl-interfaces.sh b/tools/lint/utils/generate-exempt-aidl-interfaces.sh new file mode 100755 index 000000000000..44dcdd74fe06 --- /dev/null +++ b/tools/lint/utils/generate-exempt-aidl-interfaces.sh @@ -0,0 +1,59 @@ +# +# Copyright (C) 2024 The Android Open 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. +# + +# Create a directory for the results and a nested temporary directory. +mkdir -p $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp + +# Create a copy of `AndroidGlobalLintChecker.jar` to restore it afterwards. +cp $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar \ + $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar + +# Configure the environment variable required for running the lint check on the entire source tree. +export ANDROID_LINT_CHECK=PermissionAnnotationExemptAidlInterfaces + +# Build the target corresponding to the lint checks present in the `utils` directory. +m AndroidUtilsLintChecker + +# Replace `AndroidGlobalLintChecker.jar` with the newly built `jar` file. +cp $ANDROID_BUILD_TOP/out/host/linux-x86/framework/AndroidUtilsLintChecker.jar \ + $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar; + +# Run the lint check on the entire source tree. +m lint-check + +# Copy the archive containing the results of `lint-check` into the temporary directory. +cp $ANDROID_BUILD_TOP/out/soong/lint-report-text.zip \ + $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp + +cd $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp + +# Unzip the archive containing the results of `lint-check`. +unzip lint-report-text.zip + +# Concatenate the results of `lint-check` into a single string. +concatenated_reports=$(find . -type f | xargs cat) + +# Extract the fully qualified names of the AIDL Interfaces from the concatenated results. Output +# this list into `out/soong/exempt_aidl_interfaces_generator_output/exempt_aidl_interfaces`. +echo $concatenated_reports | grep -Eo '\"([a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]*\",' | sort | uniq > ../exempt_aidl_interfaces + +# Remove the temporary directory. +rm -rf $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp + +# Restore the original copy of `AndroidGlobalLintChecker.jar` and delete the copy. +cp $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar \ + $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar +rm $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt index 0115339a68b7..245e802df49b 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt @@ -53,7 +53,7 @@ class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder { .setMessageId(key) .setMessage(log.messageString) .setLevel( - ProtoLogLevel.forNumber(log.logLevel.ordinal + 1)) + ProtoLogLevel.forNumber(log.logLevel.id)) .setGroupId(groupId) .setLocation(log.position) ) |