diff options
400 files changed, 12342 insertions, 2460 deletions
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/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/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 4d61f418af10..c0c81df465e2 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -125,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 d873f5757b29..daa15f05d942 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -8661,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 { @@ -10246,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"); @@ -11937,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) { @@ -12018,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) { @@ -12073,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) { @@ -12309,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) { @@ -12489,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) { @@ -12572,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(); } @@ -14273,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 { @@ -17178,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) { 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/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/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/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/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/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..2ac2ae916e58 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -28,7 +28,8 @@ import android.annotation.TestApi; import android.app.AppOpsManager; 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.ravenwood.annotation.RavenwoodReplace; import android.ravenwood.annotation.RavenwoodThrow; import android.text.TextUtils; @@ -233,8 +234,7 @@ import java.util.function.IntFunction; * {@link #readSparseArray(ClassLoader, Class)}. */ @RavenwoodKeepWholeClass -@RavenwoodNativeSubstitutionClass( - "com.android.platform.test.ravenwood.nativesubstitution.Parcel_host") +@RavenwoodRedirectionClass("Parcel_host") public final class Parcel { private static final boolean DEBUG_RECYCLE = false; @@ -387,6 +387,7 @@ public final class Parcel { private static final int SIZE_COMPLEX_TYPE = 1; @CriticalNative + @RavenwoodRedirect private static native void nativeMarkSensitive(long nativePtr); @FastNative @RavenwoodThrow @@ -395,86 +396,126 @@ public final class Parcel { @RavenwoodThrow private static native boolean nativeIsForRpc(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native int nativeDataSize(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native int nativeDataAvail(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native int nativeDataPosition(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native int nativeDataCapacity(long nativePtr); @FastNative + @RavenwoodRedirect private static native void nativeSetDataSize(long nativePtr, int size); @CriticalNative + @RavenwoodRedirect private static native void nativeSetDataPosition(long nativePtr, int pos); @FastNative + @RavenwoodRedirect private static native void nativeSetDataCapacity(long nativePtr, int size); @CriticalNative + @RavenwoodRedirect private static native boolean nativePushAllowFds(long nativePtr, boolean allowFds); @CriticalNative + @RavenwoodRedirect private static native void nativeRestoreAllowFds(long nativePtr, boolean lastValue); + @RavenwoodRedirect private static native void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len); + @RavenwoodRedirect private static native void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len); @CriticalNative + @RavenwoodRedirect private static native int nativeWriteInt(long nativePtr, int val); @CriticalNative + @RavenwoodRedirect private static native int nativeWriteLong(long nativePtr, long val); @CriticalNative + @RavenwoodRedirect private static native int nativeWriteFloat(long nativePtr, float val); @CriticalNative + @RavenwoodRedirect private static native int nativeWriteDouble(long nativePtr, double val); @RavenwoodThrow private static native void nativeSignalExceptionForError(int error); @FastNative + @RavenwoodRedirect private static native void nativeWriteString8(long nativePtr, String val); @FastNative + @RavenwoodRedirect private static native void nativeWriteString16(long nativePtr, String val); @FastNative @RavenwoodThrow private static native void nativeWriteStrongBinder(long nativePtr, IBinder val); @FastNative + @RavenwoodRedirect private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val); + @RavenwoodRedirect private static native byte[] nativeCreateByteArray(long nativePtr); + @RavenwoodRedirect private static native boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen); + @RavenwoodRedirect private static native byte[] nativeReadBlob(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native int nativeReadInt(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native long nativeReadLong(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native float nativeReadFloat(long nativePtr); @CriticalNative + @RavenwoodRedirect private static native double nativeReadDouble(long nativePtr); @FastNative + @RavenwoodRedirect private static native String nativeReadString8(long nativePtr); @FastNative + @RavenwoodRedirect private static native String nativeReadString16(long nativePtr); @FastNative @RavenwoodThrow private static native IBinder nativeReadStrongBinder(long nativePtr); @FastNative + @RavenwoodRedirect private static native FileDescriptor nativeReadFileDescriptor(long nativePtr); + @RavenwoodRedirect private static native long nativeCreate(); + @RavenwoodRedirect private static native void nativeFreeBuffer(long nativePtr); + @RavenwoodRedirect private static native void nativeDestroy(long nativePtr); + @RavenwoodRedirect private static native byte[] nativeMarshall(long nativePtr); + @RavenwoodRedirect private static native void nativeUnmarshall( long nativePtr, byte[] data, int offset, int length); + @RavenwoodRedirect private static native int nativeCompareData(long thisNativePtr, long otherNativePtr); + @RavenwoodRedirect private static native boolean nativeCompareDataInRange( long ptrA, int offsetA, long ptrB, int offsetB, int length); + @RavenwoodRedirect private static native void nativeAppendFrom( long thisNativePtr, long otherNativePtr, int offset, int length); @CriticalNative + @RavenwoodRedirect private static native boolean nativeHasFileDescriptors(long nativePtr); + @RavenwoodRedirect private static native boolean nativeHasFileDescriptorsInRange( long nativePtr, int offset, int length); + @RavenwoodRedirect private static native boolean nativeHasBinders(long nativePtr); + @RavenwoodRedirect private static native boolean nativeHasBindersInRange( long nativePtr, int offset, int length); @RavenwoodThrow 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/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/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/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/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/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/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/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/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 12d326486e77..032ac4283712 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -3025,6 +3025,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Override public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) { this.splitCodePaths = splitCodePaths; + this.mSplits = null; // reset for paths changed if (splitCodePaths != null) { int size = splitCodePaths.length; for (int index = 0; index < size; index++) { 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 960509addf0c..e440dc9053fd 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -480,8 +480,19 @@ public class PerfettoProtoLogImpl extends IProtoLogClient.Stub implements IProto 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) { @@ -688,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); 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 index d69a66c27bb8..1e6ba309c046 100644 --- a/core/java/com/android/internal/protolog/Utils.java +++ b/core/java/com/android/internal/protolog/Utils.java @@ -40,6 +40,11 @@ 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 -> { 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/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/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/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/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/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/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/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt index d8e131e8de1d..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 @@ -36,8 +36,7 @@ import org.junit.Rule import org.junit.Test @Ignore("Test Base Class") -abstract class ResizeAppWithCornerResize -constructor( +abstract class ResizeAppWithCornerResize( val rotation: Rotation = Rotation.ROTATION_0, val horizontalChange: Int = 200, val verticalChange: Int = -200, @@ -79,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/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/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/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/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/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/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/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 index 09271ac29061..3d4133732915 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt @@ -19,7 +19,7 @@ package com.android.settingslib.datastore import android.content.SharedPreferences /** Interface of key-value store. */ -interface KeyValueStore { +interface KeyValueStore : KeyedObservable<String> { /** Returns if the storage contains persistent value of given key. */ fun contains(key: String): Boolean 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/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..7c89592adedc 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 " @@ -1396,3 +1406,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 f3577fab8686..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 @@ -395,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. @@ -427,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/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index af1e62d327f2..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 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/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/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/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/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/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..9464c75eeb71 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 @@ -24,7 +24,6 @@ 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,6 +38,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.fakeShadeRepository import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.shade.shared.flag.DualShade @@ -66,18 +66,18 @@ 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 shadeRepository by lazy { kosmos.fakeShadeRepository } + 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 @@ -497,4 +497,24 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(shadeMode).isEqualTo(ShadeMode.Dual) } + + @Test + fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() = + testScope.runTest { + // Ensure isShadeLayoutWide is collected. + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + 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) + 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/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/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/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/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/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/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/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/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/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/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/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/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/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/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 73e86a2be4aa..3cd91be469c1 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. */ 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..b8d2dd2a764f 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 @@ -16,6 +16,7 @@ package com.android.systemui.shade.domain.interactor +import androidx.annotation.FloatRange import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardRepository @@ -104,6 +105,16 @@ constructor( override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide + @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 (shadeRepository.isShadeLayoutWide.value) 0.8f else 0.5f + } + override val shadeMode: StateFlow<ShadeMode> = isShadeLayoutWide .map(this::determineShadeMode) 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/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/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/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/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/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/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/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/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/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/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/ravenwood/Android.bp b/ravenwood/Android.bp index 9b0c8e554d64..333fe4c8147f 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -127,6 +127,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/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java index f237ba908507..1d182da5e7fd 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -27,6 +27,9 @@ 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; @@ -35,7 +38,7 @@ import org.junit.runners.model.TestClass; * 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() { } @@ -56,20 +59,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,7 +106,7 @@ 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) { // Keep track of the current class. @@ -113,7 +132,7 @@ 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) { getStats().onTestFinished(sCurrentClassDescription, description, 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..0059360a0a29 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -36,7 +36,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; @@ -92,8 +91,6 @@ public class RavenwoodRuleImpl { Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); } - RuntimeInit.redirectLogStreams(); - android.os.Process.init$ravenwood(rule.mUid, rule.mPid); android.os.Binder.init$ravenwood(); setSystemProperties(rule.mSystemProperties); diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 7d991663f4b1..bfde9cb7099e 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -23,7 +23,6 @@ import static java.lang.annotation.ElementType.TYPE; import android.util.Log; -import com.android.ravenwood.common.RavenwoodCommonUtils; import com.android.ravenwood.common.SneakyThrow; import org.junit.Assume; @@ -75,7 +74,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}) @@ -142,16 +141,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 +157,8 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde try { mTestClass = new TestClass(testClass); + onRunnerInitializing(); + /* * If the class has @DisabledOnRavenwood, then we'll delegate to * ClassSkippingTestRunner, which simply skips it. @@ -186,10 +180,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 +193,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 +227,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 +240,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); 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/Parcel_host.java b/ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java index cb00b3e758fa..720f1d227326 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java +++ b/ravenwood/runtime-helper-src/framework/android/os/Parcel_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.system.ErrnoException; import android.system.Os; @@ -527,4 +527,4 @@ public class Parcel_host { } return false; } -}
\ No newline at end of file +} 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/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java index 0f955e772445..c519204d0586 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,9 @@ */ package com.android.platform.test.ravenwood.runtimehelper; +import android.system.ErrnoException; +import android.system.Os; + import com.android.ravenwood.common.RavenwoodCommonUtils; import java.io.File; @@ -37,6 +40,14 @@ public class ClassLoadHook { private static final boolean SKIP_LOADING_LIBANDROID = "1".equals(System.getenv( "RAVENWOOD_SKIP_LOADING_LIBANDROID")); + /** + * If set to 1, and if $ANDROID_LOG_TAGS isn't set, we enable the verbose logging. + * + * (See also InitLogging() in http://ac/system/libbase/logging.cpp) + */ + private static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv( + "RAVENWOOD_VERBOSE")); + public static final String CORE_NATIVE_CLASSES = "core_native_classes"; public static final String ICU_DATA_PATH = "icu.data.path"; public static final String KEYBOARD_PATHS = "keyboard_paths"; @@ -123,6 +134,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); 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/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java index 6effeb3058d9..094723814e17 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java @@ -16,9 +16,6 @@ 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.AppSearchBatchResult; import android.app.appsearch.AppSearchManager; @@ -26,6 +23,8 @@ 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; @@ -43,14 +42,10 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; -/** - * A future API wrapper of {@link AppSearchSession} APIs. - */ -@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) +/** 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 AppSearchManager mAppSearchManager; private final AndroidFuture<AppSearchResult<AppSearchSession>> mSettableSessionFuture; public FutureAppSearchSession( @@ -62,22 +57,21 @@ public class FutureAppSearchSession implements Closeable { Objects.requireNonNull(appSearchContext); mExecutor = executor; - mAppSearchManager = appSearchManager; mSettableSessionFuture = new AndroidFuture<>(); - mAppSearchManager.createSearchSession( + appSearchManager.createSearchSession( appSearchContext, mExecutor, mSettableSessionFuture::complete); } /** Converts a failed app search result codes into an exception. */ @NonNull - private static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) { + 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()); + 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()); }; } @@ -140,14 +134,16 @@ public class FutureAppSearchSession implements Closeable { /** 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<>(); + return getSessionAsync() + .thenCompose( + session -> { + AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture = + new AndroidFuture<>(); - session.put(putDocumentsRequest, mExecutor, batchResultFuture::complete); - return batchResultFuture; - }); + session.put( + putDocumentsRequest, mExecutor, batchResultFuture::complete); + return batchResultFuture; + }); } /** @@ -155,10 +151,9 @@ public class FutureAppSearchSession implements Closeable { * of search provided. */ public AndroidFuture<FutureSearchResults> search( - @NonNull String queryExpression, - @NonNull SearchSpec searchSpec) { - return getSessionAsync().thenApply( - session -> session.search(queryExpression, searchSpec)) + @NonNull String queryExpression, @NonNull SearchSpec searchSpec) { + return getSessionAsync() + .thenApply(session -> session.search(queryExpression, searchSpec)) .thenApply(result -> new FutureSearchResults(result, mExecutor)); } @@ -176,8 +171,8 @@ public class FutureAppSearchSession implements Closeable { private final SearchResults mSearchResults; private final Executor mExecutor; - public FutureSearchResults(@NonNull SearchResults searchResults, - @NonNull Executor executor) { + public FutureSearchResults( + @NonNull SearchResults searchResults, @NonNull Executor executor) { mSearchResults = Objects.requireNonNull(searchResults); mExecutor = Objects.requireNonNull(executor); } @@ -187,15 +182,68 @@ public class FutureAppSearchSession implements Closeable { new AndroidFuture<>(); mSearchResults.getNextPage(mExecutor, nextPageFuture::complete); - return nextPageFuture.thenApply(result -> { - if (result.isSuccess()) { - return result.getResultValue(); - } else { - throw new RuntimeException( - failedResultToException(result)); - } - }); + 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/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/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/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/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/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/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/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/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/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 4fcf27d62a1a..38ef5b8cedb9 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -3403,8 +3403,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/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 43a285cba4b9..2856eb45ebd1 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1019,7 +1019,9 @@ final class InstallPackageHelper { && scanInstallPackages(requests, createdAppId, versionInfos)) { List<ReconciledPackage> reconciledPackages = reconcileInstallPackages(requests, versionInfos); - if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) { + if (reconciledPackages != null + && renameAndUpdatePaths(requests) + && commitInstallPackages(reconciledPackages)) { success = true; } } @@ -1029,24 +1031,49 @@ final class InstallPackageHelper { } } - private boolean prepareInstallPackages(List<InstallRequest> requests) { - // TODO: will remove the locking after doRename is moved out of prepare + private boolean renameAndUpdatePaths(List<InstallRequest> requests) { try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) { for (InstallRequest request : requests) { + ParsedPackage parsedPackage = request.getParsedPackage(); + final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0; + if (isApex) { + continue; + } try { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage"); - request.onPrepareStarted(); - preparePackageLI(request); - } catch (PrepareFailure prepareFailure) { - request.setError(prepareFailure.error, - prepareFailure.getMessage()); - request.setOriginPackage(prepareFailure.mConflictingPackage); - request.setOriginPermission(prepareFailure.mConflictingPermission); + doRenameLI(request, parsedPackage); + setUpFsVerity(parsedPackage); + } catch (Installer.InstallerException | IOException | DigestException + | NoSuchAlgorithmException | PrepareFailure e) { + request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP, + "Failed to set up verity: " + e); return false; - } finally { - request.onPrepareFinished(); - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } + + // update paths that are set before renaming + PackageSetting scannedPackageSetting = request.getScannedPackageSetting(); + scannedPackageSetting.setPath(new File(parsedPackage.getPath())); + scannedPackageSetting.setLegacyNativeLibraryPath( + parsedPackage.getNativeLibraryRootDir()); + } + return true; + } + } + + private boolean prepareInstallPackages(List<InstallRequest> requests) { + for (InstallRequest request : requests) { + try { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage"); + request.onPrepareStarted(); + preparePackage(request); + } catch (PrepareFailure prepareFailure) { + request.setError(prepareFailure.error, + prepareFailure.getMessage()); + request.setOriginPackage(prepareFailure.mConflictingPackage); + request.setOriginPermission(prepareFailure.mConflictingPermission); + return false; + } finally { + request.onPrepareFinished(); + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } } return true; @@ -1231,8 +1258,7 @@ final class InstallPackageHelper { return newProp != null && newProp.getBoolean(); } - @GuardedBy("mPm.mInstallLock") - private void preparePackageLI(InstallRequest request) throws PrepareFailure { + private void preparePackage(InstallRequest request) throws PrepareFailure { final int[] allUsers = mPm.mUserManager.getUserIds(); final int installFlags = request.getInstallFlags(); final boolean onExternal = request.getVolumeUuid() != null; @@ -1739,18 +1765,7 @@ final class InstallPackageHelper { } } - if (!isApex) { - doRenameLI(request, parsedPackage); - - try { - setUpFsVerity(parsedPackage); - } catch (Installer.InstallerException | IOException | DigestException - | NoSuchAlgorithmException e) { - throw PrepareFailure.ofInternalError( - "Failed to set up verity: " + e, - PackageManagerException.INTERNAL_ERROR_VERITY_SETUP); - } - } else { + if (isApex) { // Use the path returned by apexd parsedPackage.setPath(request.getApexInfo().modulePath); parsedPackage.setBaseApkPath(request.getApexInfo().modulePath); @@ -1882,10 +1897,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 @@ -2086,7 +2107,21 @@ final class InstallPackageHelper { // Reflect the rename in scanned details try { - parsedPackage.setPath(afterCodeFile.getCanonicalPath()); + String afterCanonicalPath = afterCodeFile.getCanonicalPath(); + String beforeCanonicalPath = beforeCodeFile.getCanonicalPath(); + parsedPackage.setPath(afterCanonicalPath); + + parsedPackage.setNativeLibraryDir( + parsedPackage.getNativeLibraryDir() + .replace(beforeCanonicalPath, afterCanonicalPath)); + parsedPackage.setNativeLibraryRootDir( + parsedPackage.getNativeLibraryRootDir() + .replace(beforeCanonicalPath, afterCanonicalPath)); + String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir(); + if (secondaryNativeLibraryDir != null) { + parsedPackage.setSecondaryNativeLibraryDir( + secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath)); + } } catch (IOException e) { Slog.e(TAG, "Failed to get path: " + afterCodeFile, e); throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE, 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/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/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..107d31e4e25c 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; 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/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/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 649fa9c6e5f0..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 diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp index e681fa8fec29..b5cf98697d54 100644 --- a/services/tests/appfunctions/Android.bp +++ b/services/tests/appfunctions/Android.bp @@ -37,7 +37,7 @@ android_test { "androidx.test.runner", "androidx.test.ext.truth", "platform-test-annotations", - "services.core", + "services.appfunctions", "servicestests-core-utils", "truth", "frameworks-base-testutils", 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/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/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/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/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/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..189de6bdb44a 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,32 @@ 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( + var assertion = assertThrows(RuntimeException.class, () -> implSpy.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, - new Object[]{5}); + 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(implSpy, never()).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( + LogLevel.INFO), any()); + verify(sReader).getViewerString(eq(1234L)); + + Truth.assertThat(assertion).hasMessageThat() + .contains("Failed to get log message with hash 1234 and args (5)"); } @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 +422,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 +437,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 +460,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 +473,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 +494,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 +504,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 +520,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 +543,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 +563,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 +588,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); - ProtoLogImpl.setSingleInstance(mProtoLog); + ProtoLogImpl.setSingleInstance(sProtoLog); ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1, 0b11, true); } finally { @@ -599,7 +612,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 +654,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 +683,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 +754,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 +777,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 +788,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 +811,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 +840,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 +858,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); @@ -853,6 +866,19 @@ public class PerfettoProtoLogImplTest { .isEqualTo("This message should also be logged 567"); } + @Test + public void throwsOnLogToLogcatForProcessedMessageMissingLoadedDefinition() { + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); + var protolog = new PerfettoProtoLogImpl(TestProtoLogGroup.values()); + + var exception = assertThrows(RuntimeException.class, () -> { + protolog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 123, 0, new Object[0]); + }); + + Truth.assertThat(exception).hasMessageThat() + .contains("Failed to get log message with hash 123"); + } + private enum TestProtoLogGroup implements IProtoLogGroup { TEST_GROUP(true, true, false, "TEST_TAG"); 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) ) |