diff options
253 files changed, 9131 insertions, 1864 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index df4a3e5c3b35..bd17d6d2ece5 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -20,6 +20,7 @@ aconfig_declarations_group { java_aconfig_libraries: [ // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", + "android-sdk-flags-java", "android.adaptiveauth.flags-aconfig-java", "android.app.appfunctions.flags-aconfig-java", "android.app.contextualsearch.flags-aconfig-java", diff --git a/BROADCASTS_OWNERS b/BROADCASTS_OWNERS index 01f1f8a6ba57..f0cbe46ea402 100644 --- a/BROADCASTS_OWNERS +++ b/BROADCASTS_OWNERS @@ -1,5 +1,5 @@ # Bug component: 316181 -ctate@android.com -jsharkey@google.com +set noparent + sudheersai@google.com yamasani@google.com #{LAST_RESORT_SUGGESTION} diff --git a/android-sdk-flags/Android.bp b/android-sdk-flags/Android.bp new file mode 100644 index 000000000000..79a0b9a4f273 --- /dev/null +++ b/android-sdk-flags/Android.bp @@ -0,0 +1,30 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["frameworks_base_license"], +} + +aconfig_declarations { + name: "android-sdk-flags", + package: "android.sdk", + container: "system", + srcs: ["flags.aconfig"], +} + +java_aconfig_library { + name: "android-sdk-flags-java", + aconfig_declarations: "android-sdk-flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/android-sdk-flags/flags.aconfig b/android-sdk-flags/flags.aconfig new file mode 100644 index 000000000000..cfe298e187d1 --- /dev/null +++ b/android-sdk-flags/flags.aconfig @@ -0,0 +1,12 @@ +package: "android.sdk" +container: "system" + +flag { + name: "major_minor_versioning_scheme" + namespace: "android_sdk" + description: "Use the new SDK major.minor versioning scheme (e.g. Android 40.1) which replaces the old single-integer scheme (e.g. Android 15)." + bug: "350458259" + + # Use is_fixed_read_only because DeviceConfig may not be available when Build.VERSION_CODES is first accessed + is_fixed_read_only: true +} diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java index f20b1706129b..3577fcdf04d6 100644 --- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java +++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java @@ -194,7 +194,7 @@ public final class ClientSocketPerfTest { /** * Simple benchmark for the amount of time to send a given number of messages */ - @Test + // @Test Temporarily disabled @Parameters(method = "getParams") public void time(Config config) throws Exception { reset(); diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java index af3c405eab82..ac5710047db9 100644 --- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java +++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java @@ -198,7 +198,7 @@ public final class ServerSocketPerfTest { executor.awaitTermination(5, TimeUnit.SECONDS); } - @Test + // @Test Temporarily disabled @Parameters(method = "getParams") public void throughput(Config config) throws Exception { setup(config); diff --git a/apex/blobstore/OWNERS b/apex/blobstore/OWNERS index a53bbeaa8601..676cbc7eb2a3 100644 --- a/apex/blobstore/OWNERS +++ b/apex/blobstore/OWNERS @@ -1,2 +1,5 @@ +# Bug component: 25692 +set noparent + sudheersai@google.com -yamasani@google.com +yamasani@google.com #{LAST_RESORT_SUGGESTION} diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 80db264d0f44..5f5507587f72 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -23,3 +23,10 @@ flag { description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." bug: "318731461" } + +flag { + name: "cleanup_empty_jobs" + namespace: "backstage_power" + description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis." + bug: "349688611" +} diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl index 96494ec28204..11d17ca749b7 100644 --- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl +++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl @@ -85,6 +85,14 @@ interface IJobCallback { */ @UnsupportedAppUsage void jobFinished(int jobId, boolean reschedule); + + /* + * Inform JobScheduler to force finish this job because the client has lost + * the job handle. jobFinished can no longer be called from the client. + * @param jobId Unique integer used to identify this job + */ + void forceJobFinished(int jobId); + /* * Inform JobScheduler of a change in the estimated transfer payload. * diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java index e833bb95a302..52a761f8d486 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java @@ -34,15 +34,21 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; import android.os.RemoteException; +import android.system.SystemCleaner; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.Cleaner; /** * Contains the parameters used to configure/identify your job. You do not create this object * yourself, instead it is handed in to your application by the System. */ public class JobParameters implements Parcelable { + private static final String TAG = "JobParameters"; /** @hide */ public static final int INTERNAL_STOP_REASON_UNKNOWN = -1; @@ -306,6 +312,10 @@ public class JobParameters implements Parcelable { private int mStopReason = STOP_REASON_UNDEFINED; private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN; private String debugStopReason; // Human readable stop reason for debugging. + @Nullable + private JobCleanupCallback mJobCleanupCallback; + @Nullable + private Cleaner.Cleanable mCleanable; /** @hide */ public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras, @@ -326,6 +336,8 @@ public class JobParameters implements Parcelable { this.mTriggeredContentAuthorities = triggeredContentAuthorities; this.mNetwork = network; this.mJobNamespace = namespace; + this.mJobCleanupCallback = null; + this.mCleanable = null; } /** @@ -597,6 +609,8 @@ public class JobParameters implements Parcelable { mStopReason = in.readInt(); mInternalStopReason = in.readInt(); debugStopReason = in.readString(); + mJobCleanupCallback = null; + mCleanable = null; } /** @hide */ @@ -612,6 +626,54 @@ public class JobParameters implements Parcelable { this.debugStopReason = debugStopReason; } + /** @hide */ + public void initCleaner(JobCleanupCallback jobCleanupCallback) { + mJobCleanupCallback = jobCleanupCallback; + mCleanable = SystemCleaner.cleaner().register(this, mJobCleanupCallback); + } + + /** + * Lazy initialize the cleaner and enable it + * + * @hide + */ + public void enableCleaner() { + if (mJobCleanupCallback == null) { + initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId)); + } + mJobCleanupCallback.enableCleaner(); + } + + /** + * Disable the cleaner from running and unregister it + * + * @hide + */ + public void disableCleaner() { + if (mJobCleanupCallback != null) { + mJobCleanupCallback.disableCleaner(); + if (mCleanable != null) { + mCleanable.clean(); + mCleanable = null; + } + mJobCleanupCallback = null; + } + } + + /** @hide */ + @VisibleForTesting + @Nullable + public Cleaner.Cleanable getCleanable() { + return mCleanable; + } + + /** @hide */ + @VisibleForTesting + @Nullable + public JobCleanupCallback getJobCleanupCallback() { + return mJobCleanupCallback; + } + @Override public int describeContents() { return 0; @@ -647,6 +709,67 @@ public class JobParameters implements Parcelable { dest.writeString(debugStopReason); } + /** + * JobCleanupCallback is used track JobParameters leak. If the job is started + * and jobFinish is not called at the time of garbage collection of JobParameters + * instance, it is considered a job leak. Force finish the job. + * + * @hide + */ + public static class JobCleanupCallback implements Runnable { + private final IJobCallback mCallback; + private final int mJobId; + private boolean mIsCleanerEnabled; + + public JobCleanupCallback( + IJobCallback callback, + int jobId) { + mCallback = callback; + mJobId = jobId; + mIsCleanerEnabled = false; + } + + /** + * Check if the cleaner is enabled + * + * @hide + */ + public boolean isCleanerEnabled() { + return mIsCleanerEnabled; + } + + /** + * Enable the cleaner to detect JobParameter leak + * + * @hide + */ + public void enableCleaner() { + mIsCleanerEnabled = true; + } + + /** + * Disable the cleaner from running. + * + * @hide + */ + public void disableCleaner() { + mIsCleanerEnabled = false; + } + + /** @hide */ + @Override + public void run() { + if (!isCleanerEnabled()) { + return; + } + try { + mCallback.forceJobFinished(mJobId); + } catch (Exception e) { + Log.wtf(TAG, "Could not destroy running job", e); + } + } + } + public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() { @Override public JobParameters createFromParcel(Parcel in) { diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java index 79d87edff9b2..5f80c52388b4 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java @@ -165,7 +165,13 @@ public abstract class JobServiceEngine { case MSG_EXECUTE_JOB: { final JobParameters params = (JobParameters) msg.obj; try { + if (Flags.cleanupEmptyJobs()) { + params.enableCleaner(); + } boolean workOngoing = JobServiceEngine.this.onStartJob(params); + if (Flags.cleanupEmptyJobs() && !workOngoing) { + params.disableCleaner(); + } ackStartMessage(params, workOngoing); } catch (Exception e) { Log.e(TAG, "Error while executing job: " + params.getJobId()); @@ -190,6 +196,9 @@ public abstract class JobServiceEngine { IJobCallback callback = params.getCallback(); if (callback != null) { try { + if (Flags.cleanupEmptyJobs()) { + params.disableCleaner(); + } callback.jobFinished(params.getJobId(), needsReschedule); } catch (RemoteException e) { Log.e(TAG, "Error reporting job finish to system: binder has gone" + diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index be8e304a8101..ee246d84997f 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -129,6 +129,8 @@ public final class JobServiceContext implements ServiceConnection { private static final String[] VERB_STRINGS = { "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED" }; + private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:"; + private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#"; // States that a job occupies while interacting with the client. static final int VERB_BINDING = 0; @@ -292,6 +294,11 @@ public final class JobServiceContext implements ServiceConnection { } @Override + public void forceJobFinished(int jobId) { + doForceJobFinished(this, jobId); + } + + @Override public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item, long downloadBytes, long uploadBytes) { doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes); @@ -762,6 +769,35 @@ public final class JobServiceContext implements ServiceConnection { } } + /** + * This method just adds traces to evaluate jobs that leak jobparameters at the client. + * It does not stop the job. + */ + void doForceJobFinished(JobCallback cb, int jobId) { + final long ident = Binder.clearCallingIdentity(); + try { + final JobStatus executing; + synchronized (mLock) { + // not the current job, presumably it has finished in some way already + if (!verifyCallerLocked(cb)) { + return; + } + + executing = getRunningJobLocked(); + } + if (executing != null && jobId == executing.getJobId()) { + final StringBuilder stateSuffix = new StringBuilder(); + stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX); + stateSuffix.append(executing.getBatteryName()); + stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER); + stateSuffix.append(executing.getJobId()); + Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString()); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId, int workId, @BytesLong long transferredBytes) { // TODO(255393346): Make sure apps call this appropriately and monitor for abuse diff --git a/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/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 8e08a95dad70..081dfe60d28c 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -152,6 +152,16 @@ flag { } flag { + name: "fix_race_condition_in_tie_profile_lock" + namespace: "enterprise" + description: "Fix race condition in tieProfileLockIfNecessary()" + bug: "355905501" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "quiet_mode_credential_bug_fix" namespace: "enterprise" description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped." 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/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 8975191b54c1..9355937b0963 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -170,6 +170,12 @@ public interface BiometricConstants { int BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE = 20; /** + * Biometrics is not allowed to verify in apps. + * @hide + */ + int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = 21; + + /** * This constant is only used by SystemUI. It notifies SystemUI that authentication was paused * because the authentication attempt was unsuccessful. * @hide diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index 9bc46b9f382a..a4f7485fcaa5 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -94,6 +94,13 @@ public class BiometricManager { BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; /** + * Biometrics is not allowed to verify in apps. + * @hide + */ + public static final int BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS = + BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + + /** * A security vulnerability has been discovered and the sensor is unavailable until a * security update has addressed this issue. This error can be received if for example, * authentication was requested with {@link Authenticators#BIOMETRIC_STRONG}, but the diff --git a/core/java/android/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/view/View.java b/core/java/android/view/View.java index 7b4ea41554f1..0ed0e60f6b4d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -371,7 +371,7 @@ import java.util.function.Predicate; * </tr> * <tr> * <td><code>{@link #onTouchEvent(MotionEvent)}</code></td> - * <td>Called when a touch screen motion event occurs. + * <td>Called when a motion event occurs with pointers down on the view. * </td> * </tr> * <tr> @@ -17873,7 +17873,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Implement this method to handle touch screen motion events. + * Implement this method to handle pointer events. + * <p> + * This method is called to handle motion events where pointers are down on + * the view. For example, this could include touchscreen touches, stylus + * touches, or click-and-drag events from a mouse. However, it is not called + * for motion events that do not involve pointers being down, such as hover + * events or mouse scroll wheel movements. * <p> * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 8501474b70a6..4c5802ccfcf5 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -137,9 +137,13 @@ public final class WebViewDelegate { */ @Deprecated public void detachDrawGlFunctor(View containerView, long nativeDrawGLFunctor) { - ViewRootImpl viewRootImpl = containerView.getViewRootImpl(); - if (nativeDrawGLFunctor != 0 && viewRootImpl != null) { - viewRootImpl.detachFunctor(nativeDrawGLFunctor); + if (Flags.mainlineApis()) { + throw new UnsupportedOperationException(); + } else { + ViewRootImpl viewRootImpl = containerView.getViewRootImpl(); + if (nativeDrawGLFunctor != 0 && viewRootImpl != null) { + viewRootImpl.detachFunctor(nativeDrawGLFunctor); + } } } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index b5bf529fadbd..511c832a4876 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -16,6 +16,7 @@ package android.widget; +import static android.view.flags.Flags.enableTouchScrollFeedback; import static android.view.flags.Flags.viewVelocityApi; import android.annotation.ColorInt; @@ -846,6 +847,8 @@ public class ScrollView extends FrameLayout { deltaY += mTouchSlop; } } + boolean hitTopLimit = false; + boolean hitBottomLimit = false; if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; @@ -889,12 +892,14 @@ public class ScrollView extends FrameLayout { if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } + hitTopLimit = true; } else if (pulledToY > range) { mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(), 1.f - displacement); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } + hitBottomLimit = true; } if (shouldDisplayEdgeEffects() && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { @@ -902,6 +907,20 @@ public class ScrollView extends FrameLayout { } } } + + // TODO: b/360198915 - Add unit tests. + if (enableTouchScrollFeedback()) { + if (hitTopLimit || hitBottomLimit) { + initHapticScrollFeedbackProviderIfNotExists(); + mHapticScrollFeedbackProvider.onScrollLimit(vtev.getDeviceId(), + vtev.getSource(), MotionEvent.AXIS_Y, + /* isStart= */ hitTopLimit); + } else if (Math.abs(deltaY) != 0) { + initHapticScrollFeedbackProviderIfNotExists(); + mHapticScrollFeedbackProvider.onScrollProgress(vtev.getDeviceId(), + vtev.getSource(), MotionEvent.AXIS_Y, deltaY); + } + } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig new file mode 100644 index 000000000000..f0ed83be8f1e --- /dev/null +++ b/core/java/android/widget/flags/flags.aconfig @@ -0,0 +1,11 @@ +package: "android.widget.flags" +container: "system" +flag { + name: "enable_fading_view_group" + namespace: "system_performance" + description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches" + bug: "348515581" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 4cc0d8a77a2b..c316800108bd 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -69,6 +69,23 @@ public class TaskFragmentOrganizer extends WindowOrganizer { public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type"; /** + * Key to bundle {@link TaskFragmentInfo}s from the system in + * {@link #registerOrganizer(boolean, Bundle)} + * + * @hide + */ + public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info"; + + /** + * Key to bundle {@link TaskFragmentParentInfo} from the system in + * {@link #registerOrganizer(boolean, Bundle)} + * + * @hide + */ + public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO = + "key_restore_task_fragment_parent_info"; + + /** * No change set. */ @WindowManager.TransitionType diff --git a/core/java/com/android/internal/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/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index 11c220b14bcc..0ec55f958f38 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -120,6 +120,7 @@ public class LockPatternView extends View { private static final String TAG = "LockPatternView"; private OnPatternListener mOnPatternListener; + private ExternalHapticsPlayer mExternalHapticsPlayer; @UnsupportedAppUsage private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9); @@ -317,6 +318,13 @@ public class LockPatternView extends View { void onPatternDetected(List<Cell> pattern); } + /** An external haptics player for pattern updates. */ + public interface ExternalHapticsPlayer{ + + /** Perform haptic feedback when a cell is added to the pattern. */ + void performCellAddedFeedback(); + } + public LockPatternView(Context context) { this(context, null); } @@ -461,6 +469,15 @@ public class LockPatternView extends View { } /** + * Set the external haptics player for feedback on pattern detection. + * @param player The external player. + */ + @UnsupportedAppUsage + public void setExternalHapticsPlayer(ExternalHapticsPlayer player) { + mExternalHapticsPlayer = player; + } + + /** * Set the pattern explicitely (rather than waiting for the user to input * a pattern). * @param displayMode How to display the pattern. @@ -847,6 +864,16 @@ public class LockPatternView extends View { return null; } + @Override + public boolean performHapticFeedback(int feedbackConstant, int flags) { + if (mExternalHapticsPlayer != null) { + mExternalHapticsPlayer.performCellAddedFeedback(); + return true; + } else { + return super.performHapticFeedback(feedbackConstant, flags); + } + } + private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; mPattern.add(newCell); diff --git a/core/java/com/android/internal/widget/ViewGroupFader.java b/core/java/com/android/internal/widget/ViewGroupFader.java index b54023a3382e..21206c244c8f 100644 --- a/core/java/com/android/internal/widget/ViewGroupFader.java +++ b/core/java/com/android/internal/widget/ViewGroupFader.java @@ -16,12 +16,14 @@ package com.android.internal.widget; +import android.content.res.Resources; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.animation.BaseInterpolator; import android.view.animation.PathInterpolator; +import android.widget.flags.Flags; /** * This class is ported from @@ -36,7 +38,7 @@ import android.view.animation.PathInterpolator; * height of the child. When not in the top or bottom regions, children have their default alpha and * scale. */ -class ViewGroupFader { +public class ViewGroupFader { private static final float SCALE_LOWER_BOUND = 0.7f; private float mScaleLowerBound = SCALE_LOWER_BOUND; @@ -68,7 +70,7 @@ class ViewGroupFader { private BaseInterpolator mBottomInterpolator = new PathInterpolator(0.3f, 0f, 0.7f, 1f); /** Callback which is called when attempting to fade a view. */ - interface AnimationCallback { + public interface AnimationCallback { boolean shouldFadeFromTop(View view); boolean shouldFadeFromBottom(View view); @@ -82,7 +84,7 @@ class ViewGroupFader { * of the current position. */ // TODO(b/182846214): Clean up the interface design to avoid exposing too much details to users. - interface ChildViewBoundsProvider { + public interface ChildViewBoundsProvider { /** * Provide the bounds of the child view. * @@ -168,7 +170,7 @@ class ViewGroupFader { } } - ViewGroupFader( + public ViewGroupFader( ViewGroup parent, AnimationCallback callback, ChildViewBoundsProvider childViewBoundsProvider) { @@ -212,7 +214,7 @@ class ViewGroupFader { this.mContainerBoundsProvider = boundsProvider; } - void updateFade() { + public void updateFade() { mContainerBoundsProvider.provideBounds(mParent, mContainerBounds); mTopBoundPixels = mContainerBounds.height() * mChainedBoundsTop; mBottomBoundPixels = mContainerBounds.height() * mChainedBoundsBottom; @@ -221,13 +223,20 @@ class ViewGroupFader { } /** For each list element, calculate and adjust the scale and alpha based on its position */ - private void updateListElementFades(ViewGroup parent, boolean shouldFade) { + public void updateListElementFades(ViewGroup parent, boolean shouldFade) { for (int i = 0; i < parent.getChildCount(); i++) { View child = parent.getChildAt(i); if (child.getVisibility() != View.VISIBLE) { continue; } + if (Flags.enableFadingViewGroup() && Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_enableViewGroupScalingFading)) { + if (child instanceof ViewGroup) { + updateListElementFades((ViewGroup) child, true); + } + } + if (shouldFade) { fadeElement(parent, child); } @@ -312,4 +321,4 @@ class ViewGroupFader { private static float lerp(float min, float max, float fraction) { return min + (max - min) * fraction; } -} +}
\ No newline at end of file diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index 52662149b23a..e28b6462bad7 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -96,4 +96,8 @@ <!-- True if the device supports system decorations on secondary displays. --> <bool name="config_supportsSystemDecorsOnSecondaryDisplays">false</bool> + + <!-- Whether to enable scaling and fading animation to scrollviews while scrolling. + P.S this is a change only intended for wear devices. --> + <bool name="config_enableViewGroupScalingFading">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 38aff7590a42..f6267f6174b6 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -7121,4 +7121,8 @@ <!-- The maximum number of call log entries for each sim card that can be stored in the call log provider on the device. --> <integer name="config_maximumCallLogEntriesPerSim">500</integer> + + <!-- Whether to enable scaling and fading animation to scrollviews while scrolling. + P.S this is a change only intended for wear devices. --> + <bool name="config_enableViewGroupScalingFading">false</bool> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 46938948b133..3c8c04e23087 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5601,4 +5601,6 @@ <!-- Fingerprint loe notification string --> <java-symbol type="string" name="fingerprint_loe_notification_msg" /> + + <java-symbol type="bool" name="config_enableViewGroupScalingFading"/> </resources> diff --git a/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java new file mode 100644 index 000000000000..eeabc2f4e0ed --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/widget/ViewGroupFaderTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.content.Context; +import android.content.res.Resources; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.test.AndroidTestCase; +import android.view.View; +import android.view.ViewGroup; +import android.widget.flags.Flags; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ViewGroupFader}. + */ +public class ViewGroupFaderTest extends AndroidTestCase { + + private Context mContext; + private ViewGroupFader mViewGroupFader; + private Resources mResources; + + @Mock + private ViewGroup mViewGroup,mViewGroup1; + + @Mock + private ViewGroupFader mockViewGroupFader; + + @Mock + private ViewGroupFader.AnimationCallback mAnimationCallback; + + @Mock + private ViewGroupFader.ChildViewBoundsProvider mChildViewBoundsProvider; + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + final Context mContext = getInstrumentation().getContext(); + mResources = spy(mContext.getResources()); + when(mResources.getBoolean(com.android.internal.R.bool.config_enableViewGroupScalingFading)) + .thenReturn(true); + when(mViewGroup.getResources()).thenReturn(mResources); + + mViewGroupFader = new ViewGroupFader( + mViewGroup, + mAnimationCallback, + mChildViewBoundsProvider); + } + + /** This test checks that for each child of the parent viewgroup, + * updateListElementFades is called for each of its child, when the Flag is set to true + */ + @Test + @EnableFlags(Flags.FLAG_ENABLE_FADING_VIEW_GROUP) + public void testFadingAndScrollingAnimationWorking_FlagOn() { + mViewGroup.addView(mViewGroup1); + mViewGroupFader.updateFade(); + + for (int i = 0; i < mViewGroup.getChildCount(); i++) { + View child = mViewGroup.getChildAt(i); + verify(mockViewGroupFader).updateListElementFades((ViewGroup)child,true); + } + } + + /** This test checks that for each child of the parent viewgroup, + * updateListElementFades is never called for each of its child, when the Flag is set to false + */ + @Test + public void testFadingAndScrollingAnimationNotWorking_FlagOff() { + mViewGroup.addView(mViewGroup1); + mViewGroupFader.updateFade(); + + for (int i = 0; i < mViewGroup.getChildCount(); i++) { + View child = mViewGroup.getChildAt(i); + verify(mockViewGroupFader,never()).updateListElementFades((ViewGroup)child,true); + } + } +}
\ No newline at end of file diff --git a/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/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/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/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/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java index 71cc8df80cad..422656c6d387 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -38,7 +38,6 @@ import android.view.IWindow; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -105,7 +104,7 @@ public final class BackgroundWindowManager extends WindowlessWindowManager { @Override protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) { - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + final SurfaceControl.Builder builder = new SurfaceControl.Builder() .setColorLayer() .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) .setFormat(PixelFormat.RGB_888) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 4df649ca8c93..f060158766fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -497,13 +497,8 @@ public class PipAnimationController { mCurrentValue = value; } - boolean shouldApplyCornerRadius() { - return !isOutPipDirection(mTransitionDirection); - } - boolean shouldApplyShadowRadius() { - return !isOutPipDirection(mTransitionDirection) - && !isRemovePipDirection(mTransitionDirection); + return !isRemovePipDirection(mTransitionDirection); } boolean inScaleTransition() { @@ -556,7 +551,7 @@ public class PipAnimationController { final float alpha = getStartValue() * (1 - fraction) + getEndValue() * fraction; setCurrentValue(alpha); getSurfaceTransactionHelper().alpha(tx, leash, alpha) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, destinationBounds, alpha)) { tx.apply(); @@ -572,7 +567,7 @@ public class PipAnimationController { getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); @@ -686,13 +681,11 @@ public class PipAnimationController { getSurfaceTransactionHelper().scaleAndCrop(tx, leash, adjustedSourceRectHint, initialSourceValue, bounds, insets, isInPipDirection, fraction); - if (shouldApplyCornerRadius()) { - final Rect sourceBounds = new Rect(initialContainerRect); - sourceBounds.inset(insets); - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + final Rect sourceBounds = new Rect(initialContainerRect); + sourceBounds.inset(insets); + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); } if (!handlePipTransaction(leash, tx, bounds, /* alpha= */ 1f)) { tx.apply(); @@ -741,11 +734,9 @@ public class PipAnimationController { .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, rotationDelta == ROTATION_270 /* clockwise */); - if (shouldApplyCornerRadius()) { - getSurfaceTransactionHelper() - .round(tx, leash, sourceBounds, bounds) - .shadow(tx, leash, shouldApplyShadowRadius()); - } + getSurfaceTransactionHelper() + .round(tx, leash, sourceBounds, bounds) + .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) { tx.apply(); } @@ -761,7 +752,7 @@ public class PipAnimationController { void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() .alpha(tx, leash, 1f) - .round(tx, leash, shouldApplyCornerRadius()) + .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); tx.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/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 f3113dce94a4..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 @@ -104,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; @@ -171,8 +170,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private static final String TAG = StageCoordinator.class.getSimpleName(); - private final SurfaceSession mSurfaceSession = new SurfaceSession(); - private final StageTaskListener mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); private final StageTaskListener mSideStage; @@ -334,7 +331,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayId, mMainStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); mSideStage = new StageTaskListener( @@ -343,7 +339,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDisplayId, mSideStageListener, mSyncQueue, - mSurfaceSession, iconProvider, mWindowDecorViewModel); mDisplayController = displayController; 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 29b5114d87e6..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; @@ -93,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; @@ -108,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); @@ -203,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); 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/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/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/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/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index 751275b9e167..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; @@ -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 af288c81616d..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; @@ -134,11 +132,11 @@ public class SplitTransitionTests extends ShellTestCase { doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire(); mSplitLayout = SplitTestUtils.createMockSplitLayout(); mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + 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 ce343b8a7fa9..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; @@ -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 8b5cb97505c0..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); } 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/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt index a07be79579eb..e0d16aab1e07 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt @@ -97,7 +97,7 @@ class ResizeVeilTest : ShellTestCase() { .thenReturn(spyResizeVeilSurfaceBuilder) doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory - .create(eq("Resize veil background of Task=" + taskInfo.taskId), any())) + .create(eq("Resize veil background of Task=" + taskInfo.taskId))) .thenReturn(spyBackgroundSurfaceBuilder) doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build() whenever(mockSurfaceControlBuilderFactory diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp index e6182454ad8a..5955915c9fcd 100644 --- a/libs/androidfw/AssetManager.cpp +++ b/libs/androidfw/AssetManager.cpp @@ -1420,18 +1420,20 @@ void AssetManager::mergeInfoLocked(SortedVector<AssetDir::FileInfo>* pMergedInfo Mutex AssetManager::SharedZip::gLock; DefaultKeyedVector<String8, wp<AssetManager::SharedZip> > AssetManager::SharedZip::gOpen; -AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen) - : mPath(path), mZipFile(NULL), mModWhen(modWhen), - mResourceTableAsset(NULL), mResourceTable(NULL) -{ - if (kIsDebug) { - ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); - } - ALOGV("+++ opening zip '%s'\n", mPath.c_str()); - mZipFile = ZipFileRO::open(mPath.c_str()); - if (mZipFile == NULL) { - ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); - } +AssetManager::SharedZip::SharedZip(const String8& path, ModDate modWhen) + : mPath(path), + mZipFile(NULL), + mModWhen(modWhen), + mResourceTableAsset(NULL), + mResourceTable(NULL) { + if (kIsDebug) { + ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); + } + ALOGV("+++ opening zip '%s'\n", mPath.c_str()); + mZipFile = ZipFileRO::open(mPath.c_str()); + if (mZipFile == NULL) { + ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); + } } AssetManager::SharedZip::SharedZip(int fd, const String8& path) @@ -1453,7 +1455,7 @@ sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path, bool createIfNotPresent) { AutoMutex _l(gLock); - time_t modWhen = getFileModDate(path.c_str()); + auto modWhen = getFileModDate(path.c_str()); sp<SharedZip> zip = gOpen.valueFor(path).promote(); if (zip != NULL && zip->mModWhen == modWhen) { return zip; @@ -1520,8 +1522,8 @@ ResTable* AssetManager::SharedZip::setResourceTable(ResTable* res) bool AssetManager::SharedZip::isUpToDate() { - time_t modWhen = getFileModDate(mPath.c_str()); - return mModWhen == modWhen; + auto modWhen = getFileModDate(mPath.c_str()); + return mModWhen == modWhen; } void AssetManager::SharedZip::addOverlay(const asset_path& ap) diff --git a/libs/androidfw/include/androidfw/AssetManager.h b/libs/androidfw/include/androidfw/AssetManager.h index ce0985b38986..376c881ea376 100644 --- a/libs/androidfw/include/androidfw/AssetManager.h +++ b/libs/androidfw/include/androidfw/AssetManager.h @@ -280,21 +280,21 @@ private: ~SharedZip(); private: - SharedZip(const String8& path, time_t modWhen); - SharedZip(int fd, const String8& path); - SharedZip(); // <-- not implemented + SharedZip(const String8& path, ModDate modWhen); + SharedZip(int fd, const String8& path); + SharedZip(); // <-- not implemented - String8 mPath; - ZipFileRO* mZipFile; - time_t mModWhen; + String8 mPath; + ZipFileRO* mZipFile; + ModDate mModWhen; - Asset* mResourceTableAsset; - ResTable* mResourceTable; + Asset* mResourceTableAsset; + ResTable* mResourceTable; - Vector<asset_path> mOverlays; + Vector<asset_path> mOverlays; - static Mutex gLock; - static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; + static Mutex gLock; + static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; }; /* diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 64b1f0c6ed03..98f1aa86f2db 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -25,8 +25,9 @@ #include "android-base/macros.h" #include "android-base/unique_fd.h" #include "androidfw/ConfigDescription.h" -#include "androidfw/StringPiece.h" #include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" namespace android { @@ -202,7 +203,7 @@ class LoadedIdmap { android::base::unique_fd idmap_fd_; std::string_view overlay_apk_path_; std::string_view target_apk_path_; - time_t idmap_last_mod_time_; + ModDate idmap_last_mod_time_; private: DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index 077609d20d55..09ae40c35369 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once -#include <sys/types.h> +#include <time.h> // // Handy utility functions and portability code. // -#ifndef _LIBS_ANDROID_FW_MISC_H -#define _LIBS_ANDROID_FW_MISC_H namespace android { @@ -41,15 +40,35 @@ typedef enum FileType { } FileType; /* get the file's type; follows symlinks */ FileType getFileType(const char* fileName); -/* get the file's modification date; returns -1 w/errno set on failure */ -time_t getFileModDate(const char* fileName); + +// MinGW doesn't support nanosecond resolution in stat() modification time, and given +// that it only matters on the device it's ok to keep it at the second level there. +#ifdef _WIN32 +using ModDate = time_t; +inline constexpr ModDate kInvalidModDate = ModDate(-1); +inline constexpr unsigned long long kModDateResolutionNs = 1ull * 1000 * 1000 * 1000; +inline time_t toTimeT(ModDate m) { + return m; +} +#else +using ModDate = timespec; +inline constexpr ModDate kInvalidModDate = {-1, -1}; +inline constexpr unsigned long long kModDateResolutionNs = 1; +inline time_t toTimeT(ModDate m) { + return m.tv_sec; +} +#endif + +/* get the file's modification date; returns kInvalidModDate w/errno set on failure */ +ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ -time_t getFileModDate(int fd); +ModDate getFileModDate(int fd); // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); -}; // namespace android +} // namespace android -#endif // _LIBS_ANDROID_FW_MISC_H +// Whoever uses getFileModDate() will need this as well +bool operator==(const timespec& l, const timespec& r); diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 93dcaf549a90..9bdaf18a116a 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -28,11 +28,13 @@ #include <sys/vfs.h> #endif // __linux__ -#include <cstring> -#include <cstdio> #include <errno.h> #include <sys/stat.h> +#include <cstdio> +#include <cstring> +#include <tuple> + namespace android { /* @@ -73,27 +75,32 @@ FileType getFileType(const char* fileName) } } -/* - * Get a file's modification date. - */ -time_t getFileModDate(const char* fileName) { - struct stat sb; - if (stat(fileName, &sb) < 0) { - return (time_t)-1; - } - return sb.st_mtime; +static ModDate getModDate(const struct stat& st) { +#ifdef _WIN32 + return st.st_mtime; +#else + return st.st_mtim; +#endif } -time_t getFileModDate(int fd) { - struct stat sb; - if (fstat(fd, &sb) < 0) { - return (time_t)-1; - } - if (sb.st_nlink <= 0) { - errno = ENOENT; - return (time_t)-1; - } - return sb.st_mtime; +ModDate getFileModDate(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + return kInvalidModDate; + } + return getModDate(sb); +} + +ModDate getFileModDate(int fd) { + struct stat sb; + if (fstat(fd, &sb) < 0) { + return kInvalidModDate; + } + if (sb.st_nlink <= 0) { + errno = ENOENT; + return kInvalidModDate; + } + return getModDate(sb); } #ifndef __linux__ @@ -124,4 +131,8 @@ bool isReadonlyFilesystem(int fd) { } #endif // __linux__ -}; // namespace android +} // namespace android + +bool operator==(const timespec& l, const timespec& r) { + return std::tie(l.tv_sec, l.tv_nsec) == std::tie(r.tv_sec, l.tv_nsec); +} diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 60aa7d88925d..cb2e56f5f5e4 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include <chrono> +#include <thread> + #include "android-base/file.h" #include "androidfw/ApkAssets.h" #include "androidfw/AssetManager2.h" @@ -27,6 +30,7 @@ #include "data/overlayable/R.h" #include "data/system/R.h" +using namespace std::chrono_literals; using ::testing::NotNull; namespace overlay = com::android::overlay; @@ -218,10 +222,13 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { unlink(temp_file.path); ASSERT_FALSE(apk_assets->IsUpToDate()); - sleep(2); + + const auto sleep_duration = + std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); + std::this_thread::sleep_for(sleep_duration); base::WriteStringToFile("hello", temp_file.path); - sleep(2); + std::this_thread::sleep_for(sleep_duration); ASSERT_FALSE(apk_assets->IsUpToDate()); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java index ff09084e24cd..c4173ed999f3 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java @@ -460,7 +460,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { event.startTracking(); return true; } @@ -479,7 +479,7 @@ public class PrintActivity extends Activity implements RemotePrintDocument.Updat return true; } - if (keyCode == KeyEvent.KEYCODE_BACK + if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) && event.isTracking() && !event.isCanceled()) { if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened() && !hasErrors()) { diff --git a/packages/SettingsLib/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/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/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 892f778ff8c0..ad14035d9d0a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -288,6 +288,16 @@ flag { } flag { + name: "qs_quick_rebind_active_tiles" + namespace: "systemui" + description: "Rebind active custom tiles quickly." + bug: "362526228" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "coroutine_tracing" namespace: "systemui" description: "Adds thread-local data to System UI's global coroutine scopes to " @@ -606,16 +616,6 @@ flag { } flag { - name: "screenshot_save_image_exporter" - namespace: "systemui" - description: "Save all screenshots using ImageExporter" - bug: "352308052" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "screenshot_ui_controller_refactor" namespace: "systemui" description: "Simplify and refactor ScreenshotController" 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/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/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/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt index 3b0057d87048..e531e654cd34 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -73,6 +74,7 @@ class CommunalDreamStartableTest : SysuiTestCase() { keyguardInteractor = kosmos.keyguardInteractor, keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor, dreamManager = dreamManager, + communalSceneInteractor = kosmos.communalSceneInteractor, bgScope = kosmos.applicationCoroutineScope, ) .apply { start() } @@ -158,6 +160,36 @@ class CommunalDreamStartableTest : SysuiTestCase() { } } + @Test + fun shouldNotStartDreamWhenLaunchingWidget() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setDreaming(false) + powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON) + kosmos.communalSceneInteractor.setIsLaunchingWidget(true) + whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true) + runCurrent() + + transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB) + + verify(dreamManager, never()).startDream() + } + + @Test + fun shouldNotStartDreamWhenOccluded() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setDreaming(false) + powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON) + keyguardRepository.setKeyguardOccluded(true) + whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true) + runCurrent() + + transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB) + + verify(dreamManager, never()).startDream() + } + private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = from, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt index af76b088787e..af76b088787e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 4b132c4276ea..a0bb01797f2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -18,9 +18,12 @@ package com.android.systemui.scene.ui.viewmodel +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.view.MotionEvent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.fakeFalsingManager @@ -37,6 +40,10 @@ import com.android.systemui.scene.shared.logger.sceneLogger import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -60,6 +67,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val testScope by lazy { kosmos.testScope } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } + private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } private val falsingManager by lazy { kosmos.fakeFalsingManager } @@ -75,6 +83,8 @@ class SceneContainerViewModelTest : SysuiTestCase() { sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, powerInteractor = kosmos.powerInteractor, + shadeInteractor = kosmos.shadeInteractor, + splitEdgeDetector = kosmos.splitEdgeDetector, logger = kosmos.sceneLogger, motionEventHandlerReceiver = { motionEventHandler -> this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler @@ -287,4 +297,48 @@ class SceneContainerViewModelTest : SysuiTestCase() { assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade) } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun edgeDetector_singleShade_usesDefaultEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(false) + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + + assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun edgeDetector_splitShade_usesDefaultEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(true) + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + + assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() = + testScope.runTest { + val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode) + fakeShadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt new file mode 100644 index 000000000000..3d76d280b2cc --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd +import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFailsWith +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SplitEdgeDetectorTest : SysuiTestCase() { + + private val edgeSize = 40 + private val screenWidth = 800 + private val screenHeight = 600 + + private var edgeSplitFraction = 0.7f + + private val underTest = + SplitEdgeDetector( + topEdgeSplitFraction = { edgeSplitFraction }, + edgeSize = edgeSize.dp, + ) + + @Test + fun source_noEdge_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth / 2, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeVerticallyOnTopLeft_detectsTopLeft() { + val detectedEdge = + swipeVerticallyFrom( + x = 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopLeft) + } + + @Test + fun source_swipeHorizontallyOnTopLeft_detectsLeft() { + val detectedEdge = + swipeHorizontallyFrom( + x = 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(Left) + } + + @Test + fun source_swipeVerticallyOnTopRight_detectsTopRight() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopRight) + } + + @Test + fun source_swipeHorizontallyOnTopRight_detectsRight() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(Right) + } + + @Test + fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() { + val detectedEdge = + swipeVerticallyFrom( + x = (screenWidth * edgeSplitFraction).toInt() - 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopLeft) + } + + @Test + fun source_swipeVerticallyToRightOfSplit_detectsTopRight() { + val detectedEdge = + swipeVerticallyFrom( + x = (screenWidth * edgeSplitFraction).toInt() + 1, + y = edgeSize - 1, + ) + assertThat(detectedEdge).isEqualTo(TopRight) + } + + @Test + fun source_edgeSplitFractionUpdatesDynamically() { + val middleX = (screenWidth * 0.5f).toInt() + val topY = 0 + + // Split closer to the right; middle of screen is considered "left". + edgeSplitFraction = 0.6f + assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft) + + // Split closer to the left; middle of screen is considered "right". + edgeSplitFraction = 0.4f + assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight) + + // Illegal fraction. + edgeSplitFraction = 1.2f + assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } + + // Illegal fraction. + edgeSplitFraction = -0.3f + assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) } + } + + @Test + fun source_swipeVerticallyOnBottom_detectsBottom() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth / 3, + y = screenHeight - (edgeSize / 2), + ) + assertThat(detectedEdge).isEqualTo(Bottom) + } + + @Test + fun source_swipeHorizontallyOnBottom_detectsNothing() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth / 3, + y = screenHeight - (edgeSize - 1), + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeHorizontallyOnLeft_detectsLeft() { + val detectedEdge = + swipeHorizontallyFrom( + x = edgeSize - 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isEqualTo(Left) + } + + @Test + fun source_swipeVerticallyOnLeft_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = edgeSize - 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun source_swipeHorizontallyOnRight_detectsRight() { + val detectedEdge = + swipeHorizontallyFrom( + x = screenWidth - edgeSize + 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isEqualTo(Right) + } + + @Test + fun source_swipeVerticallyOnRight_detectsNothing() { + val detectedEdge = + swipeVerticallyFrom( + x = screenWidth - edgeSize + 1, + y = screenHeight / 2, + ) + assertThat(detectedEdge).isNull() + } + + @Test + fun resolve_startInLtr_resolvesLeft() { + val resolvedEdge = Start.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(Left) + } + + @Test + fun resolve_startInRtl_resolvesRight() { + val resolvedEdge = Start.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(Right) + } + + @Test + fun resolve_endInLtr_resolvesRight() { + val resolvedEdge = End.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(Right) + } + + @Test + fun resolve_endInRtl_resolvesLeft() { + val resolvedEdge = End.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(Left) + } + + @Test + fun resolve_topStartInLtr_resolvesTopLeft() { + val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(TopLeft) + } + + @Test + fun resolve_topStartInRtl_resolvesTopRight() { + val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(TopRight) + } + + @Test + fun resolve_topEndInLtr_resolvesTopRight() { + val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr) + assertThat(resolvedEdge).isEqualTo(TopRight) + } + + @Test + fun resolve_topEndInRtl_resolvesTopLeft() { + val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl) + assertThat(resolvedEdge).isEqualTo(TopLeft) + } + + private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { + return swipeFrom(x, y, Orientation.Vertical) + } + + private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? { + return swipeFrom(x, y, Orientation.Horizontal) + } + + private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? { + return underTest.source( + layoutSize = IntSize(width = screenWidth, height = screenHeight), + position = IntOffset(x, y), + density = Density(1f), + orientation = orientation, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index 3283ea154b3f..d163abf66b05 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -19,12 +19,9 @@ package com.android.systemui.shade.domain.interactor import android.app.StatusBarManager.DISABLE2_NONE import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE import android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -39,10 +36,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState -import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.phone.dozeParameters @@ -66,18 +60,17 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { - val kosmos = testKosmos() - val testScope = kosmos.testScope - val configurationRepository by lazy { kosmos.fakeConfigurationRepository } - val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } - val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } - val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } - val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } - val powerRepository by lazy { kosmos.fakePowerRepository } - val shadeTestUtil by lazy { kosmos.shadeTestUtil } - val userRepository by lazy { kosmos.fakeUserRepository } - val userSetupRepository by lazy { kosmos.fakeUserSetupRepository } - val dozeParameters by lazy { kosmos.dozeParameters } + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository } + private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } + private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val powerRepository by lazy { kosmos.fakePowerRepository } + private val shadeTestUtil by lazy { kosmos.shadeTestUtil } + private val userRepository by lazy { kosmos.fakeUserRepository } + private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository } + private val dozeParameters by lazy { kosmos.dozeParameters } lateinit var underTest: ShadeInteractorImpl @@ -142,9 +135,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { userSetupRepository.setUserSetUp(true) disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NOTIFICATION_SHADE, - ) + DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -158,9 +149,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { userSetupRepository.setUserSetUp(true) disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_QUICK_SETTINGS, - ) + DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS) val actual by collectLastValue(underTest.isExpandToQsEnabled) assertThat(actual).isFalse() @@ -171,10 +160,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) userSetupRepository.setUserSetUp(true) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) keyguardRepository.setIsDozing(true) @@ -188,10 +174,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) @@ -205,10 +188,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userRepository.setSettings(UserSwitcherSettingsModel(isSimpleUserSwitcher = false)) @@ -222,10 +202,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -250,10 +227,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -262,17 +236,12 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { // WHEN QS is disabled disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_QUICK_SETTINGS, - ) + DisableFlagsModel(disable2 = DISABLE2_QUICK_SETTINGS) // THEN expand is disabled assertThat(actual).isFalse() // WHEN QS is enabled - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) // THEN expand is enabled assertThat(actual).isTrue() } @@ -282,10 +251,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { testScope.runTest { deviceProvisioningRepository.setDeviceProvisioned(true) keyguardRepository.setIsDozing(false) - disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NONE, - ) + disableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = DISABLE2_NONE) userSetupRepository.setUserSetUp(true) val actual by collectLastValue(underTest.isExpandToQsEnabled) @@ -359,9 +325,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { ) ) keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - to = DozeStateModel.DOZE_AOD, - ) + DozeTransitionModel(to = DozeStateModel.DOZE_AOD) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isFalse() @@ -385,9 +349,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { ) ) keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - to = DozeStateModel.DOZE_PULSING, - ) + DozeTransitionModel(to = DozeStateModel.DOZE_PULSING) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isTrue() @@ -450,51 +412,9 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { lastSleepReason = WakeSleepReason.OTHER, ) keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - transitionState = TransitionState.STARTED, - ) + TransitionStep(transitionState = TransitionState.STARTED) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) assertThat(isShadeTouchable).isTrue() } - - @Test - @DisableFlags(DualShade.FLAG_NAME) - fun legacyShadeMode_narrowScreen_singleShade() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) - - assertThat(shadeMode).isEqualTo(ShadeMode.Single) - } - - @Test - @DisableFlags(DualShade.FLAG_NAME) - fun legacyShadeMode_wideScreen_splitShade() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) - - assertThat(shadeMode).isEqualTo(ShadeMode.Split) - } - - @Test - @EnableFlags(DualShade.FLAG_NAME) - fun shadeMode_wideScreen_isDual() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(true) - - assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - } - - @Test - @EnableFlags(DualShade.FLAG_NAME) - fun shadeMode_narrowScreen_isDual() = - testScope.runTest { - val shadeMode by collectLastValue(underTest.shadeMode) - kosmos.shadeRepository.setShadeLayoutWide(false) - - assertThat(shadeMode).isEqualTo(ShadeMode.Dual) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt new file mode 100644 index 000000000000..2a2817b9af73 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorImplTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.domain.interactor + +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShadeModeInteractorImplTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: ShadeModeInteractor + + @Before + fun setUp() { + underTest = kosmos.shadeModeInteractor + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_narrowScreen_singleShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Single) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun legacyShadeMode_wideScreen_splitShade() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Split) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_wideScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun shadeMode_narrowScreen_isDual() = + testScope.runTest { + val shadeMode by collectLastValue(underTest.shadeMode) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(shadeMode).isEqualTo(ShadeMode.Dual) + } + + @Test + fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() = + testScope.runTest { + // Ensure isShadeLayoutWide is collected. + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + kosmos.shadeRepository.setShadeLayoutWide(false) + + assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f) + } + + @Test + fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() = + testScope.runTest { + // Ensure isShadeLayoutWide is collected. + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + kosmos.shadeRepository.setShadeLayoutWide(true) + + assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 840aa92548c8..26e1a4d9e961 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -26,6 +26,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.andSceneContainer @@ -36,6 +37,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository @@ -51,6 +53,7 @@ import com.android.systemui.util.ui.isAnimating import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -153,7 +156,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenNoNotifs() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -196,7 +199,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -217,7 +220,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun shouldShowEmptyShadeView_trueWhenLockedShade() = testScope.runTest { val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -315,7 +318,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -333,7 +336,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenLockedShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -351,7 +354,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenKeyguard() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -366,7 +369,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenUserNotSetUp() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -384,7 +387,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenStartingToSleep() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -402,7 +405,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenQsExpandedDefault() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -421,7 +424,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() = testScope.runTest { - val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView) + val shouldIncludeFooterView by collectFooterViewVisibility() val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs @@ -444,7 +447,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_falseWhenRemoteInputActive() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -462,7 +465,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_animatesWhenShade() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -478,7 +481,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun shouldIncludeFooterView_notAnimatingOnKeyguard() = testScope.runTest { - val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) + val shouldInclude by collectFooterViewVisibility() // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -492,6 +495,22 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @EnableSceneContainer + fun shouldShowFooterView_falseWhenShadeIsClosed() = + testScope.runTest { + val shouldShow by collectLastValue(underTest.shouldShowFooterView) + + // WHEN shade is closed + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + shadeTestUtil.setShadeExpansion(0f) + runCurrent() + + // THEN footer is hidden + assertThat(shouldShow?.value).isFalse() + } + + @Test + @DisableSceneContainer fun shouldHideFooterView_trueWhenShadeIsClosed() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -506,6 +525,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @DisableSceneContainer fun shouldHideFooterView_falseWhenShadeIsOpen() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -520,6 +540,7 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @Test + @DisableSceneContainer fun shouldHideFooterView_falseWhenQSPartiallyOpen() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -642,4 +663,10 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas assertThat(animationsEnabled).isTrue() } + + private fun TestScope.collectFooterViewVisibility() = + collectLastValue( + if (SceneContainerFlag.isEnabled) underTest.shouldShowFooterView + else underTest.shouldIncludeFooterView + ) } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index dd84bc6989a4..92e5432ad243 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -271,7 +271,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mLatencyTracker, mFalsingCollector, emergencyButtonController, mMessageAreaControllerFactory, - mDevicePostureController, mFeatureFlags, mSelectedUserInteractor); + mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, + mMSDLPlayer); } else if (keyguardInputView instanceof KeyguardPasswordView) { return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index caa74780538e..f74d93e1d88d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -36,6 +36,7 @@ import com.android.internal.widget.LockPatternView.Cell; import com.android.internal.widget.LockscreenCredential; import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; +import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper; import com.android.systemui.classifier.FalsingClassifier; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; @@ -43,6 +44,8 @@ import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.google.android.msdl.domain.MSDLPlayer; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -67,6 +70,7 @@ public class KeyguardPatternViewController private LockPatternView mLockPatternView; private CountDownTimer mCountdownTimer; private AsyncTask<?, ?, ?> mPendingLockCheck; + private MSDLPlayer mMSDLPlayer; private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() { @Override @@ -75,6 +79,10 @@ public class KeyguardPatternViewController } }; + private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> { + BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView); + }; + /** * Useful for clearing out the wrong pattern after a delay */ @@ -166,6 +174,10 @@ public class KeyguardPatternViewController boolean isValidPattern) { boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId; if (matched) { + BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback( + /* authenticationSucceeded= */true, + /* player =*/mMSDLPlayer + ); getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0); if (dismissKeyguard) { mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); @@ -173,6 +185,10 @@ public class KeyguardPatternViewController getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern); } } else { + BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback( + /* authenticationSucceeded= */false, + /* player =*/mMSDLPlayer + ); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); if (isValidPattern) { getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs); @@ -200,7 +216,7 @@ public class KeyguardPatternViewController EmergencyButtonController emergencyButtonController, KeyguardMessageAreaController.Factory messageAreaControllerFactory, DevicePostureController postureController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) { super(view, securityMode, keyguardSecurityCallback, emergencyButtonController, messageAreaControllerFactory, featureFlags, selectedUserInteractor); mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -212,6 +228,7 @@ public class KeyguardPatternViewController featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE)); mLockPatternView = mView.findViewById(R.id.lockPatternView); mPostureController = postureController; + mMSDLPlayer = msdlPlayer; } @Override @@ -249,6 +266,7 @@ public class KeyguardPatternViewController if (deadline != 0) { handleAttemptLockout(deadline); } + mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer); } @Override @@ -262,6 +280,7 @@ public class KeyguardPatternViewController cancelBtn.setOnClickListener(null); } mPostureController.removeCallback(mPostureCallback); + mLockPatternView.setExternalHapticsPlayer(null); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java index 2c97d62d690e..4d5e717536f6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonModeObserver.java @@ -28,6 +28,7 @@ import android.util.Log; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -68,8 +69,9 @@ public class AccessibilityButtonModeObserver extends } @Inject - public AccessibilityButtonModeObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_MODE); + public AccessibilityButtonModeObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_MODE); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java index 53a21b329594..1363b1c12332 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserver.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -49,8 +50,9 @@ public class AccessibilityButtonTargetsObserver extends } @Inject - public AccessibilityButtonTargetsObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); + public AccessibilityButtonTargetsObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java index c94487848b81..736217a699fd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserver.java @@ -24,6 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import javax.inject.Inject; @@ -49,8 +50,9 @@ public class AccessibilityGestureTargetsObserver extends } @Inject - public AccessibilityGestureTargetsObserver(Context context, UserTracker userTracker) { - super(context, userTracker, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); + public AccessibilityGestureTargetsObserver( + Context context, UserTracker userTracker, SecureSettings secureSettings) { + super(context, userTracker, secureSettings, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java index 443441f1ef48..eb4de6837d41 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java @@ -18,6 +18,9 @@ package com.android.systemui.accessibility; import static android.view.WindowManager.LayoutParams; +import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance; +import static com.android.systemui.Flags.enableViewCaptureTracing; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -29,8 +32,8 @@ import android.util.MathUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.WindowManager; +import com.android.app.viewcapture.ViewCaptureAwareWindowManager; import com.android.systemui.res.R; /** @@ -70,11 +73,12 @@ public abstract class MirrorWindowControl { * @see #setDefaultPosition(LayoutParams) */ private final Point mControlPosition = new Point(); - private final WindowManager mWindowManager; + private final ViewCaptureAwareWindowManager mWindowManager; MirrorWindowControl(Context context) { mContext = context; - mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext, + enableViewCaptureTracing()); } public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java index 326773fb5bef..c50cf85feccb 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.List; @@ -48,6 +49,7 @@ public abstract class SecureSettingsContentObserver<T> { private final UserTracker mUserTracker; @VisibleForTesting final ContentObserver mContentObserver; + private final SecureSettings mSecureSettings; private final String mKey; @@ -55,7 +57,7 @@ public abstract class SecureSettingsContentObserver<T> { final List<T> mListeners = new ArrayList<>(); protected SecureSettingsContentObserver(Context context, UserTracker userTracker, - String secureSettingsKey) { + SecureSettings secureSettings, String secureSettingsKey) { mKey = secureSettingsKey; mContentResolver = context.getContentResolver(); mUserTracker = userTracker; @@ -65,6 +67,7 @@ public abstract class SecureSettingsContentObserver<T> { updateValueChanged(); } }; + mSecureSettings = secureSettings; } /** @@ -80,9 +83,8 @@ public abstract class SecureSettingsContentObserver<T> { } if (mListeners.size() == 1) { - mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(mKey), /* notifyForDescendants= */ - false, mContentObserver, UserHandle.USER_ALL); + mSecureSettings.registerContentObserverForUserAsync(Settings.Secure.getUriFor(mKey), + /* notifyForDescendants= */ false, mContentObserver, UserHandle.USER_ALL); } } @@ -97,7 +99,7 @@ public abstract class SecureSettingsContentObserver<T> { mListeners.remove(listener); if (mListeners.isEmpty()) { - mContentResolver.unregisterContentObserver(mContentObserver); + mSecureSettings.unregisterContentObserverAsync(mContentObserver); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/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/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/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt index c69cea4a6a5a..04393feaae37 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt @@ -21,6 +21,7 @@ import android.app.DreamManager import com.android.systemui.CoreStartable import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming import com.android.systemui.Flags.restartDreamOnUnocclude +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -55,6 +56,7 @@ constructor( private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val dreamManager: DreamManager, + private val communalSceneInteractor: CommunalSceneInteractor, @Background private val bgScope: CoroutineScope, ) : CoreStartable { /** Flow that emits when the dream should be started underneath the glanceable hub. */ @@ -66,6 +68,8 @@ constructor( not(keyguardInteractor.isDreaming), // TODO(b/362830856): Remove this workaround. keyguardInteractor.isKeyguardShowing, + not(communalSceneInteractor.isLaunchingWidget), + not(keyguardInteractor.isKeyguardOccluded), ) .filter { it } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 21a704df074e..8818c3af4916 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -202,6 +202,13 @@ public class FrameworkServicesModule { return context.getSystemService(CaptioningManager.class); } + @Provides + @Singleton + static UserScopedService<CaptioningManager> provideUserScopedCaptioningManager( + Context context) { + return new UserScopedServiceImpl<>(context, CaptioningManager.class); + } + /** */ @Provides @Singleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index db5a63bbf446..58c8a0456241 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -73,7 +73,7 @@ constructor( if (SceneContainerFlag.isEnabled) return listenForGoneToAodOrDozing() listenForGoneToDreaming() - listenForGoneToLockscreenOrHub() + listenForGoneToLockscreenOrHubOrOccluded() listenForGoneToOccluded() listenForGoneToDreamingLockscreenHosted() } @@ -89,22 +89,19 @@ constructor( */ private fun listenForGoneToOccluded() { scope.launch("$TAG#listenForGoneToOccluded") { - keyguardInteractor.showDismissibleKeyguard - .filterRelevantKeyguardState() - .sample(keyguardInteractor.isKeyguardOccluded, ::Pair) - .collect { (_, isKeyguardOccluded) -> - if (isKeyguardOccluded) { - startTransitionTo( - KeyguardState.OCCLUDED, - ownerReason = "Dismissible keyguard with occlusion" - ) - } + keyguardInteractor.showDismissibleKeyguard.filterRelevantKeyguardState().collect { + if (keyguardInteractor.isKeyguardOccluded.value) { + startTransitionTo( + KeyguardState.OCCLUDED, + ownerReason = "Dismissible keyguard with occlusion" + ) } + } } } // Primarily for when the user chooses to lock down the device - private fun listenForGoneToLockscreenOrHub() { + private fun listenForGoneToLockscreenOrHubOrOccluded() { if (KeyguardWmStateRefactor.isEnabled) { scope.launch("$TAG#listenForGoneToLockscreenOrHub") { biometricSettingsRepository.isCurrentUserInLockdown @@ -137,7 +134,7 @@ constructor( } } } else { - scope.launch("$TAG#listenForGoneToLockscreenOrHub") { + scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") { keyguardInteractor.isKeyguardShowing .filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing } .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair) @@ -145,6 +142,8 @@ constructor( val to = if (isIdleOnCommunal) { KeyguardState.GLANCEABLE_HUB + } else if (keyguardInteractor.isKeyguardOccluded.value) { + KeyguardState.OCCLUDED } else { KeyguardState.LOCKSCREEN } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index e2bb540f6645..7afc7596a994 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -80,10 +80,7 @@ constructor( } applicationScope.launch { val refreshConfig = - Config( - Type.NoTransition, - rebuildSections = listOf(smartspaceSection), - ) + Config(Type.NoTransition, rebuildSections = listOf(smartspaceSection)) configurationInteractor.onAnyConfigurationChange.collect { refreshBlueprint(refreshConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt index f33752fc04d4..12bcc7ecbab8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToOccludedTransitionViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.util.MathUtils +import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -55,8 +56,18 @@ constructor( var currentAlpha = 0f return transitionAnimation.sharedFlow( duration = 250.milliseconds, - startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements. - onStart = { currentAlpha = viewState.alpha() }, + startTime = if (lightRevealMigration()) { + 100.milliseconds // Wait for the light reveal to "hit" the LS elements. + } else { + 0.milliseconds + }, + onStart = { + if (lightRevealMigration()) { + currentAlpha = viewState.alpha() + } else { + currentAlpha = 0f + } + }, onStep = { MathUtils.lerp(currentAlpha, 0f, it) }, onCancel = { 0f }, ) diff --git a/packages/SystemUI/src/com/android/systemui/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/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/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index 00944b8d0849..834db98263f5 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene +import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.flag.DualShade import dagger.Binds import dagger.Module @@ -119,5 +122,15 @@ interface KeyguardlessSceneContainerFrameworkModule { .mapValues { checkNotNull(it.value) } ) } + + @Provides + fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { + return SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to + // replace this constant with dynamic window insets. + edgeSize = 40.dp + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 4061ad851f57..a4c7d00d0e80 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.scene +import androidx.compose.ui.unit.dp import com.android.systemui.CoreStartable import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule import com.android.systemui.scene.domain.SceneDomainModule @@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.flag.DualShade import dagger.Binds import dagger.Module @@ -129,5 +132,15 @@ interface SceneContainerFrameworkModule { .mapValues { checkNotNull(it.value) } ) } + + @Provides + fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector { + return SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to + // replace this constant with dynamic window insets. + edgeSize = 40.dp + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt index 99e554ea5595..a3132736fe33 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/NotifShadeSceneFamilyResolver.kt @@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import dagger.Binds import dagger.Module @@ -38,17 +38,17 @@ class NotifShadeSceneFamilyResolver @Inject constructor( @Application applicationScope: CoroutineScope, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : SceneResolver { override val targetFamily: SceneKey = SceneFamilies.NotifShade override val resolvedScene: StateFlow<SceneKey> = - shadeInteractor.shadeMode + shadeModeInteractor.shadeMode .map(::notifShadeScene) .stateIn( applicationScope, started = SharingStarted.Eagerly, - initialValue = notifShadeScene(shadeInteractor.shadeMode.value), + initialValue = notifShadeScene(shadeModeInteractor.shadeMode.value), ) override fun includesScene(scene: SceneKey): Boolean = scene in notifShadeScenes @@ -61,11 +61,7 @@ constructor( } companion object { - val notifShadeScenes = - setOf( - Scenes.NotificationsShade, - Scenes.Shade, - ) + val notifShadeScenes = setOf(Scenes.NotificationsShade, Scenes.Shade) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt index 2962a3ec903d..923e712af15d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/QuickSettingsSceneFamilyResolver.kt @@ -21,7 +21,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.shade.shared.model.ShadeMode import dagger.Binds import dagger.Module @@ -38,17 +38,17 @@ class QuickSettingsSceneFamilyResolver @Inject constructor( @Application applicationScope: CoroutineScope, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : SceneResolver { override val targetFamily: SceneKey = SceneFamilies.QuickSettings override val resolvedScene: StateFlow<SceneKey> = - shadeInteractor.shadeMode + shadeModeInteractor.shadeMode .map(::quickSettingsScene) .stateIn( applicationScope, started = SharingStarted.Eagerly, - initialValue = quickSettingsScene(shadeInteractor.shadeMode.value), + initialValue = quickSettingsScene(shadeModeInteractor.shadeMode.value), ) override fun includesScene(scene: SceneKey): Boolean = scene in quickSettingsScenes @@ -62,11 +62,7 @@ constructor( companion object { val quickSettingsScenes = - setOf( - Scenes.QuickSettings, - Scenes.QuickSettingsShade, - Scenes.Shade, - ) + setOf(Scenes.QuickSettings, Scenes.QuickSettingsShade, Scenes.Shade) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 4c6341b672ad..54823945a827 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -19,9 +19,11 @@ package com.android.systemui.scene.ui.viewmodel import android.view.MotionEvent import androidx.compose.runtime.getValue import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.DefaultEdgeDetector import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SwipeSourceDetector import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.classifier.Classifier @@ -33,12 +35,15 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Overlay +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map /** Models UI state for the scene container. */ class SceneContainerViewModel @@ -47,6 +52,8 @@ constructor( private val sceneInteractor: SceneInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, + private val shadeInteractor: ShadeInteractor, + private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit, ) : ExclusiveActivatable() { @@ -59,6 +66,20 @@ constructor( /** Whether the container is visible. */ val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible) + /** + * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the + * [UserAction]s for this container. + */ + val edgeDetector: SwipeSourceDetector by + hydrator.hydratedStateOf( + traceName = "edgeDetector", + initialValue = DefaultEdgeDetector, + source = + shadeInteractor.shadeMode.map { + if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector + } + ) + override suspend fun onActivated(): Nothing { try { // Sends a MotionEventHandler to the owner of the view-model so they can report diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt new file mode 100644 index 000000000000..f88bcb57a27d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.viewmodel + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.FixedSizeEdgeDetector +import com.android.compose.animation.scene.SwipeSource +import com.android.compose.animation.scene.SwipeSourceDetector + +/** + * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into + * top-left and top-right. + */ +enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) : + SwipeSource { + TopLeft(resolveEdge = { Resolved.TopLeft }), + TopRight(resolveEdge = { Resolved.TopRight }), + TopStart( + resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight } + ), + TopEnd( + resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft } + ), + Bottom(resolveEdge = { Resolved.Bottom }), + Left(resolveEdge = { Resolved.Left }), + Right(resolveEdge = { Resolved.Right }), + Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }), + End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left }); + + override fun resolve(layoutDirection: LayoutDirection): Resolved { + return resolveEdge(layoutDirection) + } + + enum class Resolved : SwipeSource.Resolved { + TopLeft, + TopRight, + Bottom, + Left, + Right, + } +} + +/** + * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the + * top edge is split in two: top-left and top-right. The split point between the two is dynamic and + * may change during runtime. + * + * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL) + * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These + * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and + * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and + * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and + * [SceneContainerEdge.Resolved.Right]. + * + * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e., + * percentage) of screen width to consider the split point between "top-left" and "top-right" + * edges. It is called on each source detection event. + * @param edgeSize The fixed size of each edge. + */ +class SplitEdgeDetector( + val topEdgeSplitFraction: () -> Float, + val edgeSize: Dp, +) : SwipeSourceDetector { + + private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize) + + override fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SceneContainerEdge.Resolved? { + val fixedEdge = + fixedEdgeDetector.source( + layoutSize, + position, + density, + orientation, + ) + return when (fixedEdge) { + Edge.Resolved.Top -> { + val topEdgeSplitFraction = topEdgeSplitFraction() + require(topEdgeSplitFraction in 0f..1f) { + "topEdgeSplitFraction must return a value between 0.0 and 1.0" + } + val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction + if (isLeftSide) SceneContainerEdge.Resolved.TopLeft + else SceneContainerEdge.Resolved.TopRight + } + Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left + Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom + Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right + null -> null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java index a77375c14f26..f69b0cb630d3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java @@ -19,7 +19,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; -import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -133,7 +132,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler private final MessageContainerController mMessageContainerController; private final AnnouncementResolver mAnnouncementResolver; private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; private boolean mAttachRequested; private boolean mDetachRequested; @@ -393,10 +391,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler // Any cleanup needed when the service is being destroyed. @Override public void onDestroy() { - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } removeWindow(); releaseMediaPlayer(); releaseContext(); @@ -598,36 +592,12 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler // Play the shutter sound to notify that we've taken a screenshot playCameraSoundIfNeeded(); - if (screenshotSaveImageExporter()) { - saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { - if (result.uri != null) { - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }); - } else { - saveScreenshotInWorkerThread( - screenshot.getUserHandle(), - /* onComplete */ finisher, - /* actionsReadyListener */ imageData -> { - if (DEBUG_CALLBACK) { - Log.d(TAG, - "returning URI to finisher (Consumer<URI>): " + imageData.uri); - } - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, - mPackageName); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }, - null); - } + saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { + if (result.uri != null) { + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }); } /** @@ -700,35 +670,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler } /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - UserHandle owner, - @NonNull Consumer<Uri> finisher, - @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener, - @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener - quickShareActionsReadyListener) { - SaveImageInBackgroundTask.SaveImageInBackgroundData - data = new SaveImageInBackgroundTask.SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; - data.owner = owner; - data.displayId = mDisplay.getDisplayId(); - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, - mScreenshotSmartActions, data, - mScreenshotNotificationSmartActionsProvider); - mSaveInBgTask.execute(); - } - - /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ private void logScreenshotResultStatus(Uri uri, UserHandle owner) { @@ -745,13 +686,6 @@ public class LegacyScreenshotController implements InteractiveScreenshotHandler } } - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) { - logScreenshotResultStatus(imageData.uri, imageData.owner); - } - private boolean isUserSetupComplete(UserHandle owner) { return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java deleted file mode 100644 index 9bc3bd842664..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot; - -import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; -import static com.android.systemui.screenshot.LogConfig.DEBUG_STORAGE; -import static com.android.systemui.screenshot.LogConfig.logTag; -import static com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipDescription; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Process; -import android.os.UserHandle; -import android.provider.DeviceConfig; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.systemui.flags.FeatureFlags; - -import com.google.common.util.concurrent.ListenableFuture; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -/** - * An AsyncTask that saves an image to the media store in the background. - */ -class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { - private static final String TAG = logTag(SaveImageInBackgroundTask.class); - - private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s"; - private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; - - /** - * POD used in the AsyncTask which saves an image in the background. - */ - static class SaveImageInBackgroundData { - public Bitmap image; - public Consumer<Uri> finisher; - public ActionsReadyListener mActionsReadyListener; - public QuickShareActionReadyListener mQuickShareActionsReadyListener; - public UserHandle owner; - public int displayId; - - void clearImage() { - image = null; - } - } - - /** - * Structure returned by the SaveImageInBackgroundTask - */ - public static class SavedImageData { - public Uri uri; - public List<Notification.Action> smartActions; - public Notification.Action quickShareAction; - public UserHandle owner; - public String subject; // Title for sharing - public Long imageTime; // Time at which screenshot was saved - - /** - * Used to reset the return data on error - */ - public void reset() { - uri = null; - smartActions = null; - quickShareAction = null; - subject = null; - imageTime = null; - } - } - - /** - * Structure returned by the QueryQuickShareInBackgroundTask - */ - static class QuickShareData { - public Notification.Action quickShareAction; - - /** - * Used to reset the return data on error - */ - public void reset() { - quickShareAction = null; - } - } - - interface ActionsReadyListener { - void onActionsReady(SavedImageData imageData); - } - - interface QuickShareActionReadyListener { - void onActionsReady(QuickShareData quickShareData); - } - - private final Context mContext; - private FeatureFlags mFlags; - private final ScreenshotSmartActions mScreenshotSmartActions; - private final SaveImageInBackgroundData mParams; - private final SavedImageData mImageData; - private final QuickShareData mQuickShareData; - - private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; - private String mScreenshotId; - private final Random mRandom = new Random(); - private final ImageExporter mImageExporter; - private long mImageTime; - - SaveImageInBackgroundTask( - Context context, - FeatureFlags flags, - ImageExporter exporter, - ScreenshotSmartActions screenshotSmartActions, - SaveImageInBackgroundData data, - ScreenshotNotificationSmartActionsProvider - screenshotNotificationSmartActionsProvider - ) { - mContext = context; - mFlags = flags; - mScreenshotSmartActions = screenshotSmartActions; - mImageData = new SavedImageData(); - mQuickShareData = new QuickShareData(); - mImageExporter = exporter; - - // Prepare all the output metadata - mParams = data; - - // Initialize screenshot notification smart actions provider. - mSmartActionsProvider = screenshotNotificationSmartActionsProvider; - } - - @Override - protected Void doInBackground(Void... paramsUnused) { - if (isCancelled()) { - if (DEBUG_STORAGE) { - Log.d(TAG, "cancelled! returning null"); - } - return null; - } - // TODO: move to constructor / from ScreenshotRequest - final UUID requestId = UUID.randomUUID(); - - Thread.currentThread().setPriority(Thread.MAX_PRIORITY); - - Bitmap image = mParams.image; - mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, requestId); - - boolean savingToOtherUser = mParams.owner != Process.myUserHandle(); - // Smart actions don't yet work for cross-user saves. - boolean smartActionsEnabled = !savingToOtherUser - && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, - true); - try { - if (smartActionsEnabled && mParams.mQuickShareActionsReadyListener != null) { - // Since Quick Share target recommendation does not rely on image URL, it is - // queried and surfaced before image compress/export. Action intent would not be - // used, because it does not contain image URL. - Notification.Action quickShare = - queryQuickShareAction(mScreenshotId, image, mParams.owner, null); - if (quickShare != null) { - mQuickShareData.quickShareAction = quickShare; - mParams.mQuickShareActionsReadyListener.onActionsReady(mQuickShareData); - } - } - - // Call synchronously here since already on a background thread. - ListenableFuture<ImageExporter.Result> future = - mImageExporter.export(Runnable::run, requestId, image, mParams.owner, - mParams.displayId); - ImageExporter.Result result = future.get(); - Log.d(TAG, "Saved screenshot: " + result); - final Uri uri = result.uri; - mImageTime = result.timestamp; - - CompletableFuture<List<Notification.Action>> smartActionsFuture = - mScreenshotSmartActions.getSmartActionsFuture( - mScreenshotId, uri, image, mSmartActionsProvider, - ScreenshotSmartActionType.REGULAR_SMART_ACTIONS, - smartActionsEnabled, mParams.owner); - List<Notification.Action> smartActions = new ArrayList<>(); - if (smartActionsEnabled) { - int timeoutMs = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, - 1000); - smartActions.addAll(buildSmartActions( - mScreenshotSmartActions.getSmartActions( - mScreenshotId, smartActionsFuture, timeoutMs, - mSmartActionsProvider, - ScreenshotSmartActionType.REGULAR_SMART_ACTIONS), - mContext)); - } - - mImageData.uri = uri; - mImageData.owner = mParams.owner; - mImageData.smartActions = smartActions; - mImageData.quickShareAction = createQuickShareAction( - mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image, - mParams.owner); - mImageData.subject = getSubjectString(mImageTime); - mImageData.imageTime = mImageTime; - - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " - + "finisher.accept(\"" + mImageData.uri + "\""); - } - mParams.finisher.accept(mImageData.uri); - mParams.image = null; - } catch (Exception e) { - // IOException/UnsupportedOperationException may be thrown if external storage is - // not mounted - Log.d(TAG, "Failed to store screenshot", e); - mParams.clearImage(); - mImageData.reset(); - mQuickShareData.reset(); - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)"); - } - mParams.finisher.accept(null); - } - - return null; - } - - /** - * Update the listener run when the saving task completes. Used to avoid showing UI for the - * first screenshot when a second one is taken. - */ - void setActionsReadyListener(ActionsReadyListener listener) { - mParams.mActionsReadyListener = listener; - } - - @Override - protected void onCancelled(Void params) { - // If we are cancelled while the task is running in the background, we may get null - // params. The finisher is expected to always be called back, so just use the baked-in - // params from the ctor in any case. - mImageData.reset(); - mQuickShareData.reset(); - mParams.mActionsReadyListener.onActionsReady(mImageData); - if (DEBUG_CALLBACK) { - Log.d(TAG, "onCancelled, calling (Consumer<Uri>) finisher.accept(null)"); - } - mParams.finisher.accept(null); - mParams.clearImage(); - } - - private List<Notification.Action> buildSmartActions( - List<Notification.Action> actions, Context context) { - List<Notification.Action> broadcastActions = new ArrayList<>(); - for (Notification.Action action : actions) { - // Proxy smart actions through {@link SmartActionsReceiver} for logging smart actions. - Bundle extras = action.getExtras(); - String actionType = extras.getString( - ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, - ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - Intent intent = new Intent(context, SmartActionsReceiver.class) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, action.actionIntent) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - addIntentExtras(mScreenshotId, intent, actionType, true /* smartActionsEnabled */); - PendingIntent broadcastIntent = PendingIntent.getBroadcast(context, - mRandom.nextInt(), - intent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title, - broadcastIntent).setContextual(true).addExtras(extras).build()); - } - return broadcastActions; - } - - private static void addIntentExtras(String screenshotId, Intent intent, String actionType, - boolean smartActionsEnabled) { - intent - .putExtra(SmartActionsReceiver.EXTRA_ACTION_TYPE, actionType) - .putExtra(SmartActionsReceiver.EXTRA_ID, screenshotId) - .putExtra(SmartActionsReceiver.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled); - } - - /** - * Wrap the quickshare intent and populate the fillin intent with the URI - */ - @VisibleForTesting - Notification.Action createQuickShareAction( - Notification.Action quickShare, String screenshotId, Uri uri, long imageTime, - Bitmap image, UserHandle user) { - if (quickShare == null) { - return null; - } else if (quickShare.actionIntent.isImmutable()) { - Notification.Action quickShareWithUri = - queryQuickShareAction(screenshotId, image, user, uri); - if (quickShareWithUri == null - || !quickShareWithUri.title.toString().contentEquals(quickShare.title)) { - return null; - } - quickShare = quickShareWithUri; - } - - Intent wrappedIntent = new Intent(mContext, SmartActionsReceiver.class) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT, quickShare.actionIntent) - .putExtra(SmartActionsReceiver.EXTRA_ACTION_INTENT_FILLIN, - createFillInIntent(uri, imageTime)) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - Bundle extras = quickShare.getExtras(); - String actionType = extras.getString( - ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, - ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - // We only query for quick share actions when smart actions are enabled, so we can assert - // that it's true here. - addIntentExtras(screenshotId, wrappedIntent, actionType, true /* smartActionsEnabled */); - PendingIntent broadcastIntent = - PendingIntent.getBroadcast(mContext, mRandom.nextInt(), wrappedIntent, - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); - return new Notification.Action.Builder(quickShare.getIcon(), quickShare.title, - broadcastIntent) - .setContextual(true) - .addExtras(extras) - .build(); - } - - private Intent createFillInIntent(Uri uri, long imageTime) { - Intent fillIn = new Intent(); - fillIn.setType("image/png"); - fillIn.putExtra(Intent.EXTRA_STREAM, uri); - fillIn.putExtra(Intent.EXTRA_SUBJECT, getSubjectString(imageTime)); - // Include URI in ClipData also, so that grantPermission picks it up. - // We don't use setData here because some apps interpret this as "to:". - ClipData clipData = new ClipData( - new ClipDescription("content", new String[]{"image/png"}), - new ClipData.Item(uri)); - fillIn.setClipData(clipData); - fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - return fillIn; - } - - /** - * Query and surface Quick Share chip if it is available. Action intent would not be used, - * because it does not contain image URL which would be populated in {@link - * #createQuickShareAction(Notification.Action, String, Uri, long, Bitmap, UserHandle)} - */ - - @VisibleForTesting - Notification.Action queryQuickShareAction( - String screenshotId, Bitmap image, UserHandle user, Uri uri) { - CompletableFuture<List<Notification.Action>> quickShareActionsFuture = - mScreenshotSmartActions.getSmartActionsFuture( - screenshotId, uri, image, mSmartActionsProvider, - ScreenshotSmartActionType.QUICK_SHARE_ACTION, - true /* smartActionsEnabled */, user); - int timeoutMs = DeviceConfig.getInt( - DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_QUICK_SHARE_ACTIONS_TIMEOUT_MS, - 500); - List<Notification.Action> quickShareActions = - mScreenshotSmartActions.getSmartActions( - screenshotId, quickShareActionsFuture, timeoutMs, - mSmartActionsProvider, - ScreenshotSmartActionType.QUICK_SHARE_ACTION); - if (!quickShareActions.isEmpty()) { - return quickShareActions.get(0); - } - return null; - } - - private static String getSubjectString(long imageTime) { - String subjectDate = DateFormat.getDateTimeInstance().format(new Date(imageTime)); - return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 7b802a2a40aa..fe58bc9f34a9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -18,7 +18,6 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static com.android.systemui.Flags.screenshotSaveImageExporter; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -124,7 +123,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { private final MessageContainerController mMessageContainerController; private final AnnouncementResolver mAnnouncementResolver; private Bitmap mScreenBitmap; - private SaveImageInBackgroundTask mSaveInBgTask; private boolean mScreenshotTakenInPortrait; private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; @@ -373,10 +371,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // Any cleanup needed when the service is being destroyed. @Override public void onDestroy() { - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } removeWindow(); releaseMediaPlayer(); releaseContext(); @@ -525,36 +519,12 @@ public class ScreenshotController implements InteractiveScreenshotHandler { // Play the shutter sound to notify that we've taken a screenshot playCameraSoundIfNeeded(); - if (screenshotSaveImageExporter()) { - saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { - if (result.uri != null) { - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }); - } else { - saveScreenshotInWorkerThread( - screenshot.getUserHandle(), - /* onComplete */ finisher, - /* actionsReadyListener */ imageData -> { - if (DEBUG_CALLBACK) { - Log.d(TAG, - "returning URI to finisher (Consumer<URI>): " + imageData.uri); - } - finisher.accept(imageData.uri); - if (imageData.uri == null) { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, - mPackageName); - mNotificationsController.notifyScreenshotError( - R.string.screenshot_failed_to_save_text); - } else { - mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); - mScreenshotHandler.post(() -> Toast.makeText(mContext, - R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); - } - }, - null); - } + saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { + if (result.uri != null) { + mScreenshotHandler.post(() -> Toast.makeText(mContext, + R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); + } + }); } /** @@ -627,35 +597,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - UserHandle owner, - @NonNull Consumer<Uri> finisher, - @Nullable SaveImageInBackgroundTask.ActionsReadyListener actionsReadyListener, - @Nullable SaveImageInBackgroundTask.QuickShareActionReadyListener - quickShareActionsReadyListener) { - SaveImageInBackgroundTask.SaveImageInBackgroundData - data = new SaveImageInBackgroundTask.SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; - data.owner = owner; - data.displayId = mDisplay.getDisplayId(); - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, - mScreenshotSmartActions, data, - mScreenshotNotificationSmartActionsProvider); - mSaveInBgTask.execute(); - } - - /** * Logs success/failure of the screenshot saving task, and shows an error if it failed. */ private void logScreenshotResultStatus(Uri uri, UserHandle owner) { @@ -672,13 +613,6 @@ public class ScreenshotController implements InteractiveScreenshotHandler { } } - /** - * Logs success/failure of the screenshot saving task, and shows an error if it failed. - */ - private void logSuccessOnActionsReady(SaveImageInBackgroundTask.SavedImageData imageData) { - logScreenshotResultStatus(imageData.uri, imageData.owner); - } - private boolean isUserSetupComplete(UserHandle owner) { return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt index 7425807b716d..99ff94605c39 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt @@ -28,6 +28,8 @@ import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractorEmptyImpl import dagger.Binds import dagger.Module @@ -75,4 +77,8 @@ abstract class ShadeEmptyImplModule { @Binds @SysUISingleton abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository + + @Binds + @SysUISingleton + abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorEmptyImpl): ShadeModeInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index da2024b4ef18..2348a110eb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -41,6 +41,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractorImpl import dagger.Binds import dagger.Module import dagger.Provides @@ -54,7 +56,7 @@ abstract class ShadeModule { @SysUISingleton fun provideBaseShadeInteractor( sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>, - sceneContainerOff: Provider<ShadeInteractorLegacyImpl> + sceneContainerOff: Provider<ShadeInteractorLegacyImpl>, ): BaseShadeInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -67,7 +69,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeController( sceneContainerOn: Provider<ShadeControllerSceneImpl>, - sceneContainerOff: Provider<ShadeControllerImpl> + sceneContainerOff: Provider<ShadeControllerImpl>, ): ShadeController { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -80,7 +82,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeAnimationInteractor( sceneContainerOn: Provider<ShadeAnimationInteractorSceneContainerImpl>, - sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl> + sceneContainerOff: Provider<ShadeAnimationInteractorLegacyImpl>, ): ShadeAnimationInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -93,7 +95,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeBackActionInteractor( sceneContainerOn: Provider<ShadeBackActionInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): ShadeBackActionInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -106,7 +108,7 @@ abstract class ShadeModule { @SysUISingleton fun provideShadeLockscreenInteractor( sceneContainerOn: Provider<ShadeLockscreenInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): ShadeLockscreenInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -119,7 +121,7 @@ abstract class ShadeModule { @SysUISingleton fun providePanelExpansionInteractor( sceneContainerOn: Provider<PanelExpansionInteractorImpl>, - sceneContainerOff: Provider<NotificationPanelViewController> + sceneContainerOff: Provider<NotificationPanelViewController>, ): PanelExpansionInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -170,4 +172,8 @@ abstract class ShadeModule { @Binds @SysUISingleton abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository + + @Binds + @SysUISingleton + abstract fun bindShadeModeInteractor(impl: ShadeModeInteractorImpl): ShadeModeInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 73e86a2be4aa..6fb96da2c186 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade.domain.interactor +import androidx.annotation.FloatRange import com.android.systemui.shade.shared.model.ShadeMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -69,6 +70,20 @@ interface ShadeInteractor : BaseShadeInteractor { * wide as the entire screen. */ val isShadeLayoutWide: StateFlow<Boolean> + + /** + * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold + * between "top-left" and "top-right" for the purposes of dual-shade invocation. + * + * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On + * wide layouts however, a larger fraction is returned because only the area of the system + * status icons is considered top-right. + * + * Note that this fraction only determines the split between the absolute left and right + * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" + * will resolve to "top-left". + */ + @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float } /** ShadeInteractor methods with implementations that differ between non-empty impls. */ @@ -130,7 +145,7 @@ interface BaseShadeInteractor { fun createAnyExpansionFlow( scope: CoroutineScope, shadeExpansion: Flow<Float>, - qsExpansion: Flow<Float> + qsExpansion: Flow<Float>, ): StateFlow<Float> { return combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) } .stateIn(scope, SharingStarted.Eagerly, 0f) diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index d51fd28d5458..6c0b55a5dd57 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -47,4 +47,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean + + override fun getTopEdgeSplitFraction(): Float = 0.5f } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 3552092d24e7..3eab02ad30d5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -24,9 +24,6 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.shade.shared.flag.DualShade -import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository @@ -54,11 +51,14 @@ constructor( keyguardRepository: KeyguardRepository, keyguardTransitionInteractor: KeyguardTransitionInteractor, powerInteractor: PowerInteractor, - private val shadeRepository: ShadeRepository, userSetupRepository: UserSetupRepository, userSwitcherInteractor: UserSwitcherInteractor, private val baseShadeInteractor: BaseShadeInteractor, -) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { + shadeModeInteractor: ShadeModeInteractor, +) : + ShadeInteractor, + BaseShadeInteractor by baseShadeInteractor, + ShadeModeInteractor by shadeModeInteractor { override val isShadeEnabled: StateFlow<Boolean> = disableFlagsRepository.disableFlags .map { it.isShadeEnabled() } @@ -102,17 +102,6 @@ constructor( } } - override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide - - override val shadeMode: StateFlow<ShadeMode> = - isShadeLayoutWide - .map(this::determineShadeMode) - .stateIn( - scope, - SharingStarted.Eagerly, - initialValue = determineShadeMode(isShadeLayoutWide.value) - ) - override val isExpandToQsEnabled: Flow<Boolean> = combine( disableFlagsRepository.disableFlags, @@ -129,12 +118,4 @@ constructor( disableFlags.isQuickSettingsEnabled() && !isDozing } - - private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { - return when { - DualShade.isEnabled -> ShadeMode.Dual - isShadeLayoutWide -> ShadeMode.Split - else -> ShadeMode.Single - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt new file mode 100644 index 000000000000..77ae679bf018 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.domain.interactor + +import androidx.annotation.FloatRange +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * Defines interface for classes that can provide state and business logic related to the mode of + * the shade. + */ +interface ShadeModeInteractor { + + /** + * The version of the shade layout to use. + * + * Note: Most likely, you want to read [isShadeLayoutWide] instead of this. + */ + val shadeMode: StateFlow<ShadeMode> + + /** + * Whether the shade layout should be wide (true) or narrow (false). + * + * In a wide layout, notifications and quick settings each take up only half the screen width + * (whether they are shown at the same time or not). In a narrow layout, they can each be as + * wide as the entire screen. + */ + val isShadeLayoutWide: StateFlow<Boolean> + + /** + * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold + * between "top-left" and "top-right" for the purposes of dual-shade invocation. + * + * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On + * wide layouts however, a larger fraction is returned because only the area of the system + * status icons is considered top-right. + * + * Note that this fraction only determines the split between the absolute left and right + * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end" + * will resolve to "top-left". + */ + @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float +} + +class ShadeModeInteractorImpl +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val repository: ShadeRepository, +) : ShadeModeInteractor { + + override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide + + override val shadeMode: StateFlow<ShadeMode> = + isShadeLayoutWide + .map(this::determineShadeMode) + .stateIn( + applicationScope, + SharingStarted.Eagerly, + initialValue = determineShadeMode(isShadeLayoutWide.value), + ) + + @FloatRange(from = 0.0, to = 1.0) + override fun getTopEdgeSplitFraction(): Float { + // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This + // assumption allows us to query its value on demand (during swipe source detection) instead + // of running another infinite coroutine. + // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based + // on the position of system-status icons in the status bar. + return if (repository.isShadeLayoutWide.value) 0.8f else 0.5f + } + + private fun determineShadeMode(isShadeLayoutWide: Boolean): ShadeMode { + return when { + DualShade.isEnabled -> ShadeMode.Dual + isShadeLayoutWide -> ShadeMode.Split + else -> ShadeMode.Single + } + } +} + +class ShadeModeInteractorEmptyImpl @Inject constructor() : ShadeModeInteractor { + + override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single) + + override val isShadeLayoutWide: StateFlow<Boolean> = MutableStateFlow(false) + + override fun getTopEdgeSplitFraction(): Float = 0.5f +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt index 37ac7c4330af..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 @@ -108,7 +108,7 @@ constructor(@NotificationInterruptLog val buffer: LogBuffer) { TAG, INFO, { bool1 = isEnabled }, - { "Cooldown enabled: $isEnabled" } + { "Cooldown enabled: $bool1" } ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 8ff1ab640442..1214440a6b65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -5404,7 +5404,9 @@ public class NotificationStackScrollLayout println(pw, "intrinsicContentHeight", mIntrinsicContentHeight); println(pw, "contentHeight", mContentHeight); println(pw, "intrinsicPadding", mIntrinsicPadding); - println(pw, "topPadding", getTopPadding()); + if (!SceneContainerFlag.isEnabled()) { + println(pw, "topPadding", getTopPadding()); + } println(pw, "bottomPadding", mBottomPadding); dumpRoundedRectClipping(pw); println(pw, "requestedClipBounds", mRequestedClipBounds); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java index 580431a13d1b..969ff1b4ffe7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java @@ -68,6 +68,7 @@ public class SectionHeaderView extends StackScrollerDecorView { if (mLabelTextId != null) { mLabelView.setText(mLabelTextId); } + mLabelView.setAccessibilityHeading(true); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index ef1bcfc45879..cccac4b479dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -682,7 +682,10 @@ public class StackScrollAlgorithm { // doesn't get updated quickly enough and can cause the footer to flash when // closing the shade. As such, we temporarily also check the ambientState directly. if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { - viewState.hidden = true; + // Note: This is no longer necessary in flexiglass. + if (!SceneContainerFlag.isEnabled()) { + viewState.hidden = true; + } } else { final float footerEnd = algorithmState.mCurrentExpandedYPosition + view.getIntrinsicHeight(); @@ -691,7 +694,6 @@ public class StackScrollAlgorithm { noSpaceForFooter || (ambientState.isClearAllInProgress() && !hasNonClearableNotifs(algorithmState)); } - } else { final boolean shadeClosed = !ambientState.isShadeExpanded(); final boolean isShelfShowing = algorithmState.firstViewInShelf != null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index d770b2003f3b..dc9615c25ada 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -188,15 +188,26 @@ constructor( .startHistoryIntent(view, /* showHistory= */ true) }, ) - launch { - viewModel.shouldIncludeFooterView.collect { animatedVisibility -> - footerView.setVisible( - /* visible = */ animatedVisibility.value, - /* animate = */ animatedVisibility.isAnimating, - ) + if (SceneContainerFlag.isEnabled) { + launch { + viewModel.shouldShowFooterView.collect { animatedVisibility -> + footerView.setVisible( + /* visible = */ animatedVisibility.value, + /* animate = */ animatedVisibility.isAnimating, + ) + } + } + } else { + launch { + viewModel.shouldIncludeFooterView.collect { animatedVisibility -> + footerView.setVisible( + /* visible = */ animatedVisibility.value, + /* animate = */ animatedVisibility.isAnimating, + ) + } } + launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } } } - launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } } disposableHandle.awaitCancellationThenDispose() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index e55492e67d02..4e2a46d78a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Notif import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent import com.android.systemui.util.ui.AnimatedValue @@ -120,6 +121,7 @@ constructor( * This essentially corresponds to having the view set to INVISIBLE. */ val shouldHideFooterView: Flow<Boolean> by lazy { + SceneContainerFlag.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(false) } else { @@ -143,6 +145,7 @@ constructor( * be hidden by another condition (see [shouldHideFooterView] above). */ val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy { + SceneContainerFlag.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(AnimatedValue.NotAnimating(false)) } else { @@ -207,6 +210,76 @@ constructor( } } + // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass. + val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { + flowOf(AnimatedValue.NotAnimating(false)) + } else { + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + userSetupInteractor.isUserSetUp, + notificationStackInteractor.isShowingOnLockscreen, + shadeInteractor.isQsFullscreen, + remoteInputInteractor.isRemoteInputActive, + shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(), + ) { + hasNotifications, + isUserSetUp, + isShowingOnLockscreen, + qsFullScreen, + isRemoteInputActive, + shadeLessThanHalfwayExpanded -> + when { + !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer until the user setup is complete, to prevent access + // to settings (b/193149550). + !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Do not show the footer if the lockscreen is visible (incl. AOD), + // except if the shade is opened on top. See also b/219680200. + // Do not animate, as that makes the footer appear briefly when + // transitioning between the shade and keyguard. + isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION + // Do not show the footer if quick settings are fully expanded (except + // for the foldable split shade view). See b/201427195 && b/222699879. + qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // Hide the footer if remote input is active (i.e. user is replying to a + // notification). See b/75984847. + isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + // If the shade is not expanded enough, the footer shouldn't be visible. + shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + else -> VisibilityChange.APPEAR_WITH_ANIMATION + } + } + .distinctUntilChanged( + // Equivalent unless visibility changes + areEquivalent = { a: VisibilityChange, b: VisibilityChange -> + a.visible == b.visible + } + ) + // Should we animate the visibility change? + .sample( + // TODO(b/322167853): This check is currently duplicated in FooterViewModel, + // but instead it should be a field in ShadeAnimationInteractor. + combine( + shadeInteractor.isShadeFullyExpanded, + shadeInteractor.isShadeTouchable, + ::Pair + ) + .onStart { emit(Pair(false, false)) } + ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> + // Animate if the shade is interactive, but NOT on the lockscreen. Having + // animations enabled while on the lockscreen makes the footer appear briefly + // when transitioning between the shade and keyguard. + val shouldAnimate = + isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate + AnimatableEvent(visibilityChange.visible, shouldAnimate) + } + .toAnimatedValueFlow() + .dumpWhileCollecting("shouldShowFooterView") + .flowOn(bgDispatcher) + } + } + enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) { DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false), DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/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/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/accessibility/AccessibilityButtonModeObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java index 4a5c1bed7b44..038ec406c3d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonModeObserverTest.java @@ -32,12 +32,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -66,7 +68,7 @@ public class AccessibilityButtonModeObserverTest extends SysuiTestCase { Settings.Secure.ACCESSIBILITY_BUTTON_MODE, Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR, MY_USER_ID); mAccessibilityButtonModeObserver = new AccessibilityButtonModeObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java index a5a7a4a09227..f5649266d0a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityButtonTargetsObserverTest.java @@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -62,7 +64,7 @@ public class AccessibilityButtonTargetsObserverTest extends SysuiTestCase { public void setUp() { when(mUserTracker.getUserId()).thenReturn(MY_USER_ID); mAccessibilityButtonTargetsObserver = new AccessibilityButtonTargetsObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java index ba990efd5162..afed12fb700b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/AccessibilityGestureTargetsObserverTest.java @@ -31,12 +31,14 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -62,7 +64,7 @@ public class AccessibilityGestureTargetsObserverTest extends SysuiTestCase { public void setUp() { when(mUserTracker.getUserId()).thenReturn(MY_USER_ID); mAccessibilityGestureTargetsObserver = new AccessibilityGestureTargetsObserver(mContext, - mUserTracker); + mUserTracker, Mockito.mock(SecureSettings.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java index 9222fc2222be..1d88b904668c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SecureSettingsContentObserverTest.java @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -72,7 +73,7 @@ public class SecureSettingsContentObserverTest extends SysuiTestCase { protected FakeSecureSettingsContentObserver(Context context, UserTracker userTracker, String secureSettingsKey) { - super(context, userTracker, secureSettingsKey); + super(context, userTracker, Mockito.mock(SecureSettings.class), secureSettingsKey); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/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/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/screenshot/SaveImageInBackgroundTaskTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt deleted file mode 100644 index 5e07aef7d8e9..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/SaveImageInBackgroundTaskTest.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.screenshot - -import android.app.Notification -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Icon -import android.net.Uri -import android.os.UserHandle -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import java.util.concurrent.CompletableFuture -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Test - -@SmallTest -class SaveImageInBackgroundTaskTest : SysuiTestCase() { - private val imageExporter = mock<ImageExporter>() - private val smartActions = mock<ScreenshotSmartActions>() - private val smartActionsProvider = mock<ScreenshotNotificationSmartActionsProvider>() - private val saveImageData = SaveImageInBackgroundTask.SaveImageInBackgroundData() - private val testScreenshotId: String = "testScreenshotId" - private val testBitmap = mock<Bitmap>() - private val testUser = UserHandle.getUserHandleForUid(0) - private val testIcon = mock<Icon>() - private val testImageTime = 1234.toLong() - private val flags = FakeFeatureFlags() - - private val smartActionsUriFuture = mock<CompletableFuture<List<Notification.Action>>>() - private val smartActionsFuture = mock<CompletableFuture<List<Notification.Action>>>() - - private val testUri: Uri = Uri.parse("testUri") - private val intent = - Intent(Intent.ACTION_SEND) - .setComponent( - ComponentName.unflattenFromString( - "com.google.android.test/com.google.android.test.TestActivity" - ) - ) - private val immutablePendingIntent = - PendingIntent.getBroadcast( - mContext, - 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - private val mutablePendingIntent = - PendingIntent.getBroadcast( - mContext, - 0, - intent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_MUTABLE - ) - - private val saveImageTask = - SaveImageInBackgroundTask( - mContext, - flags, - imageExporter, - smartActions, - saveImageData, - smartActionsProvider, - ) - - @Before - fun setup() { - whenever( - smartActions.getSmartActionsFuture( - eq(testScreenshotId), - any(Uri::class.java), - eq(testBitmap), - eq(smartActionsProvider), - any(ScreenshotSmartActionType::class.java), - any(Boolean::class.java), - eq(testUser) - ) - ) - .thenReturn(smartActionsUriFuture) - whenever( - smartActions.getSmartActionsFuture( - eq(testScreenshotId), - eq(null), - eq(testBitmap), - eq(smartActionsProvider), - any(ScreenshotSmartActionType::class.java), - any(Boolean::class.java), - eq(testUser) - ) - ) - .thenReturn(smartActionsFuture) - } - - @Test - fun testQueryQuickShare_noAction() { - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(ArrayList<Notification.Action>()) - - val quickShareAction = - saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri) - - assertNull(quickShareAction) - } - - @Test - fun testQueryQuickShare_withActions() { - val actions = ArrayList<Notification.Action>() - actions.add(constructAction("Action One", mutablePendingIntent)) - actions.add(constructAction("Action Two", mutablePendingIntent)) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.queryQuickShareAction(testScreenshotId, testBitmap, testUser, testUri)!! - - assertEquals("Action One", quickShareAction.title) - assertEquals(mutablePendingIntent, quickShareAction.actionIntent) - } - - @Test - fun testCreateQuickShareAction_originalWasNull_returnsNull() { - val quickShareAction = - saveImageTask.createQuickShareAction( - null, - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser - ) - - assertNull(quickShareAction) - } - - @Test - fun testCreateQuickShareAction_immutableIntentDifferentAction_returnsNull() { - val actions = ArrayList<Notification.Action>() - actions.add(constructAction("New Test Action", immutablePendingIntent)) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - val origAction = constructAction("Old Test Action", immutablePendingIntent) - - val quickShareAction = - saveImageTask.createQuickShareAction( - origAction, - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser, - ) - - assertNull(quickShareAction) - } - - @Test - fun testCreateQuickShareAction_mutableIntent_returnsSafeIntent() { - val actions = ArrayList<Notification.Action>() - val action = constructAction("Action One", mutablePendingIntent) - actions.add(action) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.createQuickShareAction( - constructAction("Test Action", mutablePendingIntent), - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser - ) - val quickSharePendingIntent = - quickShareAction.actionIntent.intent.extras!!.getParcelable( - SmartActionsReceiver.EXTRA_ACTION_INTENT, - PendingIntent::class.java - ) - - assertEquals("Test Action", quickShareAction.title) - assertEquals(mutablePendingIntent, quickSharePendingIntent) - } - - @Test - fun testCreateQuickShareAction_immutableIntent_returnsSafeIntent() { - val actions = ArrayList<Notification.Action>() - val action = constructAction("Test Action", immutablePendingIntent) - actions.add(action) - whenever( - smartActions.getSmartActions( - eq(testScreenshotId), - eq(smartActionsUriFuture), - any(Int::class.java), - eq(smartActionsProvider), - eq(ScreenshotSmartActionType.QUICK_SHARE_ACTION) - ) - ) - .thenReturn(actions) - - val quickShareAction = - saveImageTask.createQuickShareAction( - constructAction("Test Action", immutablePendingIntent), - testScreenshotId, - testUri, - testImageTime, - testBitmap, - testUser, - )!! - - assertEquals("Test Action", quickShareAction.title) - assertEquals( - immutablePendingIntent, - quickShareAction.actionIntent.intent.extras!!.getParcelable( - SmartActionsReceiver.EXTRA_ACTION_INTENT, - PendingIntent::class.java - ) - ) - } - - private fun constructAction(title: String, intent: PendingIntent): Notification.Action { - return Notification.Action.Builder(testIcon, title, intent).build() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 9481e5a52098..e0c4ab737511 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -431,7 +431,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFakeKeyguardRepository, mKeyguardTransitionInteractor, mPowerInteractor, - mShadeRepository, new FakeUserSetupRepository(), mock(UserSwitcherInteractor.class), new ShadeInteractorLegacyImpl( @@ -447,8 +446,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { () -> mLargeScreenHeaderHelper ), mShadeRepository - ) - ); + ), + mKosmos.getShadeModeInteractor()); SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 3f6617b32131..a52f1737117a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -217,7 +217,6 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mKeyguardRepository, keyguardTransitionInteractor, powerInteractor, - mShadeRepository, new FakeUserSetupRepository(), mUserSwitcherInteractor, new ShadeInteractorLegacyImpl( @@ -232,8 +231,8 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { deviceEntryUdfpsInteractor, () -> mLargeScreenHeaderHelper), mShadeRepository - ) - ); + ), + mKosmos.getShadeModeInteractor()); mActiveNotificationsInteractor = new ActiveNotificationsInteractor( new ActiveNotificationListRepository(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 01a3d36a05ec..1d74331e429b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -1112,9 +1112,11 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() { when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( KeyguardSecurityModel.SecurityMode.SimPin); + // Returning false means unable to show the bouncer when(mPrimaryBouncerInteractor.show(true)).thenReturn(false); when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) .thenReturn(KeyguardState.LOCKSCREEN); + mStatusBarKeyguardViewManager.onStartedWakingUp(); reset(mCentralSurfaces); // Advance past reattempts @@ -1127,6 +1129,23 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Test @DisableSceneContainer + @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART) + public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() { + when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) + .thenReturn(KeyguardState.LOCKSCREEN); + mStatusBarKeyguardViewManager.onStartedGoingToSleep(); + + reset(mCentralSurfaces); + reset(mPrimaryBouncerInteractor); + mStatusBarKeyguardViewManager.showBouncerOrKeyguard( + /* hideBouncerWhenShowing= */true, false); + verify(mCentralSurfaces).showKeyguard(); + verify(mPrimaryBouncerInteractor).hide(); + } + + + @Test + @DisableSceneContainer public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() { boolean isFalsingReset = false; when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn( diff --git a/packages/SystemUI/tests/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/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 457bd284ea8d..c60305e85b22 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -61,6 +61,7 @@ import com.android.systemui.scene.shared.model.sceneDataSource import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shade.shadeController import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel @@ -156,4 +157,5 @@ class KosmosJavaAdapter() { val scrimStartable by lazy { kosmos.scrimStartable } val sceneContainerOcclusionInteractor by lazy { kosmos.sceneContainerOcclusionInteractor } val msdlPlayer by lazy { kosmos.fakeMSDLPlayer } + val shadeModeInteractor by lazy { kosmos.shadeModeInteractor } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt index a0fc76b3d7de..4978558ff8a2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.fakeSystemClock val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by Kosmos.Fixture { @@ -39,6 +40,7 @@ val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by activityManager, mock(), fakeExecutor, + fakeSystemClock, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 55f3ed7062aa..874463819c73 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -12,6 +12,8 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.FakeOverlay import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector +import com.android.systemui.shade.domain.interactor.shadeInteractor import kotlinx.coroutines.flow.MutableStateFlow var Kosmos.sceneKeys by Fixture { @@ -70,6 +72,8 @@ val Kosmos.sceneContainerViewModel by Fixture { sceneInteractor = sceneInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, + shadeInteractor = shadeInteractor, + splitEdgeDetector = splitEdgeDetector, motionEventHandlerReceiver = {}, logger = sceneLogger ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt index ae33aead67a7..d17b5750b937 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt @@ -24,7 +24,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.shared.model.SceneFamilies -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.sceneFamilyResolvers: Map<SceneKey, SceneResolver> @@ -48,7 +48,7 @@ val Kosmos.notifShadeSceneFamilyResolver by Kosmos.Fixture { NotifShadeSceneFamilyResolver( applicationScope = applicationCoroutineScope, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, ) } @@ -56,6 +56,6 @@ val Kosmos.quickSettingsSceneFamilyResolver by Kosmos.Fixture { QuickSettingsSceneFamilyResolver( applicationScope = applicationCoroutineScope, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt new file mode 100644 index 000000000000..e0b529261c4d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene.ui.viewmodel + +import androidx.compose.ui.unit.dp +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.domain.interactor.shadeInteractor + +var Kosmos.splitEdgeDetector: SplitEdgeDetector by + Kosmos.Fixture { + SplitEdgeDetector( + topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction, + edgeSize = 40.dp, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index 54208b9cdaef..04d930c72792 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -53,7 +53,7 @@ val Kosmos.shadeInteractorLegacyImpl by scope = applicationCoroutineScope, keyguardRepository = keyguardRepository, sharedNotificationContainerInteractor = sharedNotificationContainerInteractor, - repository = shadeRepository + repository = shadeRepository, ) } var Kosmos.shadeInteractor: ShadeInteractor by Kosmos.Fixture { shadeInteractorImpl } @@ -70,6 +70,6 @@ val Kosmos.shadeInteractorImpl by userSetupRepository = userSetupRepository, userSwitcherInteractor = userSwitcherInteractor, baseShadeInteractor = baseShadeInteractor, - shadeRepository = shadeRepository, + shadeModeInteractor = shadeModeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt new file mode 100644 index 000000000000..7892e962d63d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.shade.data.repository.shadeRepository + +val Kosmos.shadeModeInteractor by Fixture { + ShadeModeInteractorImpl( + applicationScope = applicationCoroutineScope, + repository = shadeRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt new file mode 100644 index 000000000000..78763f97adc3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user.utils + +import android.os.UserHandle + +class FakeUserScopedService<T>(private val defaultImplementation: T) : UserScopedService<T> { + + private val implementations = mutableMapOf<UserHandle, T>() + + fun addImplementation(user: UserHandle, implementation: T) { + implementations[user] = implementation + } + + fun removeImplementation(user: UserHandle): T? = implementations.remove(user) + + override fun forUser(user: UserHandle): T = + implementations.getOrDefault(user, defaultImplementation) +} diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 26b0f617d971..136738fcb343 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -22,6 +22,7 @@ import android.content.pm.PackageManager; import android.graphics.GraphicBuffer; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.hardware.SyncFence; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraExtensionCharacteristics; @@ -2525,6 +2526,19 @@ public class CameraExtensionsProxyService extends Service { } @Override + public SyncFence getFence() { + if (mParcelImage.fence != null) { + try { + return SyncFence.create(mParcelImage.fence.dup()); + } catch (IOException e) { + Log.e(TAG, "Failed to parcel buffer fence!"); + } + } + + return SyncFence.createEmpty(); + } + + @Override protected final void finalize() throws Throwable { try { close(); diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 333fe4c8147f..eebe5e9fc054 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -94,6 +94,9 @@ java_library { libs: [ "ravenwood-runtime-common-ravenwood", ], + static_libs: [ + "framework-annotations-lib", // should it be "libs" instead? + ], visibility: ["//visibility:private"], } diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java new file mode 100644 index 000000000000..f9794ad5941e --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that throws from @AfterClass. + * + * Tradefed would ignore it, so instead RavenwoodAwareTestRunner would detect it and kill + * the self (test) process. + * + * Unfortunately, this behavior can't easily be tested from within this class, so for now + * it's only used for a manual test, which you can run by removing the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodAfterClassFailureTest { + public RavenwoodAfterClassFailureTest(String param) { + } + + @AfterClass + public static void afterClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + throw new RuntimeException("FAILURE"); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java new file mode 100644 index 000000000000..61fb06865545 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails in assumption in @BeforeClass. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * "ASSUMPTION_FAILED". + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodBeforeClassAssumptionFailureTest { + public RavenwoodBeforeClassAssumptionFailureTest(String param) { + } + + @BeforeClass + public static void beforeClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + Assume.assumeTrue(false); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java new file mode 100644 index 000000000000..626ce8198eeb --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails throws from @BeforeClass. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * a "FAILURE" runtime exception. + * + * In order to run the test, you'll need to remove the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodBeforeClassFailureTest { + public static final String TAG = "RavenwoodBeforeClassFailureTest"; + + public RavenwoodBeforeClassFailureTest(String param) { + } + + @BeforeClass + public static void beforeClass() { + if (!isOnRavenwood()) return; // Don't do anything on real device. + + throw new RuntimeException("FAILURE"); + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java new file mode 100644 index 000000000000..dc949c466110 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import static org.junit.Assume.assumeTrue; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails in assumption from a class rule. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * "ASSUMPTION_FAILED". + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodClassRuleAssumptionFailureTest { + public static final String TAG = "RavenwoodClassRuleFailureTest"; + + @ClassRule + public static final TestRule sClassRule = new TestRule() { + @Override + public Statement apply(Statement base, Description description) { + if (!isOnRavenwood()) { + return base; // Just run the test as-is on a real device. + } + + assumeTrue(false); + return null; // unreachable + } + }; + + public RavenwoodClassRuleAssumptionFailureTest(String param) { + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java new file mode 100644 index 000000000000..9996bec41525 --- /dev/null +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.bivalenttest.listenertests; + +import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + +/** + * Test that fails throws from a class rule. + * + * This is only used for manual tests. Make sure `atest` shows 4 test results with + * a "FAILURE" runtime exception. + * + * In order to run the test, you'll need to remove the @Ignore. + * + * TODO(b/364948126) Improve the tests and automate it. + */ +@Ignore +@RunWith(ParameterizedAndroidJunit4.class) +public class RavenwoodClassRuleFailureTest { + public static final String TAG = "RavenwoodClassRuleFailureTest"; + + @ClassRule + public static final TestRule sClassRule = new TestRule() { + @Override + public Statement apply(Statement base, Description description) { + if (!isOnRavenwood()) { + return base; // Just run the test as-is on a real device. + } + + throw new RuntimeException("FAILURE"); + } + }; + + public RavenwoodClassRuleFailureTest(String param) { + } + + @Parameters + public static List<String> getParams() { + var params = new ArrayList<String>(); + params.add("foo"); + params.add("bar"); + return params; + } + + @Test + public void test1() { + } + + @Test + public void test2() { + } +} diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java index 1d182da5e7fd..6d21e440e911 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java @@ -34,6 +34,8 @@ import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runners.model.TestClass; +import java.util.Stack; + /** * Provide hook points created by {@link RavenwoodAwareTestRunner}. */ @@ -44,6 +46,11 @@ public class RavenwoodAwareTestRunnerHook { } private static RavenwoodTestStats sStats; // lazy initialization. + + // Keep track of the current class description. + + // Test classes can be nested because of "Suite", so we need a stack to keep track. + private static final Stack<Description> sClassDescriptions = new Stack<>(); private static Description sCurrentClassDescription; private static RavenwoodTestStats getStats() { @@ -108,14 +115,15 @@ public class RavenwoodAwareTestRunnerHook { Scope scope, Order order) { Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order); - if (scope == Scope.Class && order == Order.First) { + if (scope == Scope.Class && order == Order.Outer) { // Keep track of the current class. sCurrentClassDescription = description; + sClassDescriptions.push(description); } // Class-level annotations are checked by the runner already, so we only check // method-level annotations here. - if (scope == Scope.Instance && order == Order.First) { + if (scope == Scope.Instance && order == Order.Outer) { if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood( description, true)) { getStats().onTestFinished(sCurrentClassDescription, description, Result.Skipped); @@ -134,17 +142,20 @@ public class RavenwoodAwareTestRunnerHook { Scope scope, Order order, Throwable th) { Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th); - if (scope == Scope.Instance && order == Order.First) { + if (scope == Scope.Instance && order == Order.Outer) { getStats().onTestFinished(sCurrentClassDescription, description, th == null ? Result.Passed : Result.Failed); - } else if (scope == Scope.Class && order == Order.Last) { + } else if (scope == Scope.Class && order == Order.Outer) { getStats().onClassFinished(sCurrentClassDescription); + sClassDescriptions.pop(); + sCurrentClassDescription = + sClassDescriptions.size() == 0 ? null : sClassDescriptions.peek(); } // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error. if (RavenwoodRule.private$ravenwood().isRunningDisabledTests() - && scope == Scope.Instance && order == Order.First) { + && scope == Scope.Instance && order == Order.Outer) { boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood( description, false); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java index 631f68ff1dec..3ffabefb7681 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java @@ -127,7 +127,11 @@ public class RavenwoodTestStats { int passed = 0; int skipped = 0; int failed = 0; - for (var e : mStats.get(classDescription).values()) { + var stats = mStats.get(classDescription); + if (stats == null) { + return; + } + for (var e : stats.values()) { switch (e) { case Passed: passed++; break; case Skipped: skipped++; break; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index bfde9cb7099e..dffb263e77cb 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -15,20 +15,25 @@ */ package android.platform.test.ravenwood; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod; import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.Log; import com.android.ravenwood.common.SneakyThrow; import org.junit.Assume; +import org.junit.AssumptionViolatedException; import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; import org.junit.rules.TestRule; import org.junit.runner.Description; +import org.junit.runner.Result; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; @@ -39,8 +44,11 @@ import org.junit.runner.manipulation.Orderer; import org.junit.runner.manipulation.Sortable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; import org.junit.runner.notification.RunNotifier; +import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.MultipleFailureException; import org.junit.runners.model.RunnerBuilder; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; @@ -51,6 +59,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Stack; /** * A test runner used for Ravenwood. @@ -61,7 +71,7 @@ import java.lang.reflect.InvocationTargetException; * the inner runner gets a chance to run. This can be used to initialize stuff used by the * inner runner. * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from - * the four test rules such as {@link #sImplicitClassMinRule}, which are also injected by + * the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by * the ravenizer tool. * * We use this runner to: @@ -102,28 +112,50 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde /** Order of a hook. */ public enum Order { - First, - Last, + Outer, + Inner, } // The following four rule instances will be injected to tests by the Ravenizer tool. + private static class RavenwoodClassOuterRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Outer); + } + } - public static final TestRule sImplicitClassMinRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Class, Order.First); + private static class RavenwoodClassInnerRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Inner); + } + } - public static final TestRule sImplicitClassMaxRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Last); + private static class RavenwoodInstanceOuterRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement( + base, description, Scope.Instance, Order.Outer); + } + } - public static final TestRule sImplicitInstMinRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.First); + private static class RavenwoodInstanceInnerRule implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return getCurrentRunner().updateStatement( + base, description, Scope.Instance, Order.Inner); + } + } - public static final TestRule sImplicitInstMaxRule = (base, description) -> - getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.Last); + public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule(); + public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule(); + public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule(); + public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule(); - public static final String IMPLICIT_CLASS_MIN_RULE_NAME = "sImplicitClassMinRule"; - public static final String IMPLICIT_CLASS_MAX_RULE_NAME = "sImplicitClassMaxRule"; - public static final String IMPLICIT_INST_MIN_RULE_NAME = "sImplicitInstMinRule"; - public static final String IMPLICIT_INST_MAX_RULE_NAME = "sImplicitInstMaxRule"; + public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule"; + public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule"; + public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule"; + public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule"; /** Keeps track of the runner on the current thread. */ private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>(); @@ -157,6 +189,8 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde try { mTestClass = new TestClass(testClass); + Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName()); + onRunnerInitializing(); /* @@ -261,20 +295,27 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } @Override - public void run(RunNotifier notifier) { + public void run(RunNotifier realNotifier) { + final RunNotifier notifier = new RavenwoodRunNotifier(realNotifier); + if (mRealRunner instanceof ClassSkippingTestRunner) { mRealRunner.run(notifier); RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription()); return; } + Log.v(TAG, "Starting " + mTestClass.getJavaClass().getCanonicalName()); + if (RAVENWOOD_VERBOSE_LOGGING) { + dumpDescription(getDescription()); + } + if (maybeReportExceptionFromConstructor(notifier)) { return; } sCurrentRunner.set(this); try { - runWithHooks(getDescription(), Scope.Runner, Order.First, + runWithHooks(getDescription(), Scope.Runner, Order.Outer, () -> mRealRunner.run(notifier)); } finally { sCurrentRunner.remove(); @@ -399,4 +440,217 @@ public class RavenwoodAwareTestRunner extends Runner implements Filterable, Orde } } } + + private void dumpDescription(Description desc) { + dumpDescription(desc, "[TestDescription]=", " "); + } + + private void dumpDescription(Description desc, String header, String indent) { + Log.v(TAG, indent + header + desc); + + var children = desc.getChildren(); + var childrenIndent = " " + indent; + for (int i = 0; i < children.size(); i++) { + dumpDescription(children.get(i), "#" + i + ": ", childrenIndent); + } + } + + /** + * A run notifier that wraps another notifier and provides the following features: + * - Handle a failure that happened before testStarted and testEnded (typically that means + * it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if + * individual tests in the class reported it. This is for b/364395552. + * + * - Logging. + */ + private class RavenwoodRunNotifier extends RunNotifier { + private final RunNotifier mRealNotifier; + + private final Stack<Description> mSuiteStack = new Stack<>(); + private Description mCurrentSuite = null; + private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>(); + + private boolean mBeforeTest = true; + private boolean mAfterTest = false; + + private RavenwoodRunNotifier(RunNotifier realNotifier) { + mRealNotifier = realNotifier; + } + + private boolean isInTest() { + return !mBeforeTest && !mAfterTest; + } + + @Override + public void addListener(RunListener listener) { + mRealNotifier.addListener(listener); + } + + @Override + public void removeListener(RunListener listener) { + mRealNotifier.removeListener(listener); + } + + @Override + public void addFirstListener(RunListener listener) { + mRealNotifier.addFirstListener(listener); + } + + @Override + public void fireTestRunStarted(Description description) { + Log.i(TAG, "testRunStarted: " + description); + mRealNotifier.fireTestRunStarted(description); + } + + @Override + public void fireTestRunFinished(Result result) { + Log.i(TAG, "testRunFinished: " + + result.getRunCount() + "," + + result.getFailureCount() + "," + + result.getAssumptionFailureCount() + "," + + result.getIgnoreCount()); + mRealNotifier.fireTestRunFinished(result); + } + + @Override + public void fireTestSuiteStarted(Description description) { + Log.i(TAG, "testSuiteStarted: " + description); + mRealNotifier.fireTestSuiteStarted(description); + + mBeforeTest = true; + mAfterTest = false; + + // Keep track of the current suite, needed if the outer test is a Suite, + // in which case its children are test classes. (not test methods) + mCurrentSuite = description; + mSuiteStack.push(description); + + mOutOfTestFailures.clear(); + } + + @Override + public void fireTestSuiteFinished(Description description) { + Log.i(TAG, "testSuiteFinished: " + description); + mRealNotifier.fireTestSuiteFinished(description); + + maybeHandleOutOfTestFailures(); + + mBeforeTest = true; + mAfterTest = false; + + // Restore the upper suite. + mSuiteStack.pop(); + mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek(); + } + + @Override + public void fireTestStarted(Description description) throws StoppedByUserException { + Log.i(TAG, "testStarted: " + description); + mRealNotifier.fireTestStarted(description); + + mAfterTest = false; + mBeforeTest = false; + } + + @Override + public void fireTestFailure(Failure failure) { + Log.i(TAG, "testFailure: " + failure); + + if (isInTest()) { + mRealNotifier.fireTestFailure(failure); + } else { + mOutOfTestFailures.add(failure.getException()); + } + } + + @Override + public void fireTestAssumptionFailed(Failure failure) { + Log.i(TAG, "testAssumptionFailed: " + failure); + + if (isInTest()) { + mRealNotifier.fireTestAssumptionFailed(failure); + } else { + mOutOfTestFailures.add(failure.getException()); + } + } + + @Override + public void fireTestIgnored(Description description) { + Log.i(TAG, "testIgnored: " + description); + mRealNotifier.fireTestIgnored(description); + } + + @Override + public void fireTestFinished(Description description) { + Log.i(TAG, "testFinished: " + description); + mRealNotifier.fireTestFinished(description); + + mAfterTest = true; + } + + @Override + public void pleaseStop() { + Log.w(TAG, "pleaseStop:"); + mRealNotifier.pleaseStop(); + } + + /** + * At the end of each Suite, we handle failures happened out of test methods. + * (typically in @BeforeClass or @AfterClasses) + * + * This is to work around b/364395552. + */ + private boolean maybeHandleOutOfTestFailures() { + if (mOutOfTestFailures.size() == 0) { + return false; + } + Throwable th; + if (mOutOfTestFailures.size() == 1) { + th = mOutOfTestFailures.get(0); + } else { + th = new MultipleFailureException(mOutOfTestFailures); + } + if (mBeforeTest) { + reportBeforeTestFailure(mCurrentSuite, th); + return true; + } + if (mAfterTest) { + // Unfortunately, there's no good way to report it, so kill the own process. + onCriticalError( + "Failures detected in @AfterClass, which would be swalloed by tradefed", + th); + return true; // unreachable + } + return false; + } + + private void reportBeforeTestFailure(Description suiteDesc, Throwable th) { + // If a failure happens befere running any tests, we'll need to pretend + // as if each test in the suite reported the failure, to work around b/364395552. + for (var child : suiteDesc.getChildren()) { + if (child.isSuite()) { + // If the chiil is still a "parent" -- a test class or a test suite + // -- propagate to its children. + mRealNotifier.fireTestSuiteStarted(child); + reportBeforeTestFailure(child, th); + mRealNotifier.fireTestSuiteFinished(child); + } else { + mRealNotifier.fireTestStarted(child); + Failure f = new Failure(child, th); + if (th instanceof AssumptionViolatedException) { + mRealNotifier.fireTestAssumptionFailed(f); + } else { + mRealNotifier.fireTestFailure(f); + } + mRealNotifier.fireTestFinished(child); + } + } + } + } + + private void onCriticalError(@NonNull String message, @Nullable Throwable th) { + Log.e(TAG, "Critical error! Ravenwood cannot continue. Killing self process: " + + message, th); + System.exit(1); + } } diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index 7b5bc5aeb7b6..875ce71149cd 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -15,12 +15,17 @@ */ package com.android.ravenwood.common; +import android.annotation.NonNull; +import android.annotation.Nullable; + import com.android.ravenwood.common.divergence.RavenwoodDivergence; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; @@ -33,6 +38,14 @@ public class RavenwoodCommonUtils { private static final Object sLock = new Object(); + /** + * If set to "1", we enable the verbose logging. + * + * (See also InitLogging() in http://ac/system/libbase/logging.cpp) + */ + public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv( + "RAVENWOOD_VERBOSE")); + /** Name of `libravenwood_runtime` */ private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime"; @@ -265,4 +278,12 @@ public class RavenwoodCommonUtils { method.getDeclaringClass().getName(), method.getName(), (isStatic ? "static " : ""))); } + + @NonNull + public static String getStackTraceString(@Nullable Throwable th) { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + th.printStackTrace(writer); + return stringWriter.toString(); + } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java index 97e99e50ea75..790bb1c2373b 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java @@ -15,6 +15,8 @@ */ package com.android.platform.test.ravenwood.runtimehelper; +import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; + import android.system.ErrnoException; import android.system.Os; @@ -40,14 +42,6 @@ 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"; diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt index eaef2cf6a956..bd9d96d81604 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt @@ -302,7 +302,7 @@ class RunnerRewritingAdapter private constructor( override fun visitCode() { visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_CLASS_MIN_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTSTATIC, @@ -313,7 +313,7 @@ class RunnerRewritingAdapter private constructor( visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_CLASS_MAX_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTSTATIC, @@ -361,7 +361,7 @@ class RunnerRewritingAdapter private constructor( visitVarInsn(ALOAD, 0) visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_INST_MIN_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTFIELD, @@ -373,7 +373,7 @@ class RunnerRewritingAdapter private constructor( visitVarInsn(ALOAD, 0) visitFieldInsn(Opcodes.GETSTATIC, ravenwoodTestRunnerType.internlName, - RavenwoodAwareTestRunner.IMPLICIT_INST_MAX_RULE_NAME, + RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME, testRuleType.desc ) visitFieldInsn(Opcodes.PUTFIELD, diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING new file mode 100644 index 000000000000..c7f5eeef8fa0 --- /dev/null +++ b/services/appfunctions/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "FrameworksAppFunctionsTests" + } + ] +}
\ No newline at end of file diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java index eba628dc1fba..094723814e17 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java +++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java @@ -16,15 +16,15 @@ 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; 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; @@ -42,10 +42,7 @@ 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; @@ -67,14 +64,14 @@ public class FutureAppSearchSession implements Closeable { /** 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()); }; } @@ -137,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; + }); } /** @@ -152,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)); } @@ -173,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); } @@ -184,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/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java new file mode 100644 index 000000000000..be5770b280dc --- /dev/null +++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appfunctions; + +import android.annotation.NonNull; +import android.annotation.WorkerThread; +import android.app.appsearch.SearchResult; +import android.app.appsearch.SearchSpec; +import android.util.ArrayMap; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +/** + * This class implements helper methods for synchronously interacting with AppSearch while + * synchronizing AppFunction runtime and static metadata. + */ +public class MetadataSyncAdapter { + private final FutureAppSearchSession mFutureAppSearchSession; + private final Executor mSyncExecutor; + + public MetadataSyncAdapter( + @NonNull Executor syncExecutor, + @NonNull FutureAppSearchSession futureAppSearchSession) { + mSyncExecutor = Objects.requireNonNull(syncExecutor); + mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession); + } + + /** + * This method returns a map of package names to a set of function ids that are in the static + * metadata but not in the runtime metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the static metadata but + * not in the runtime metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap); + } + + /** + * This method returns a map of package names to a set of function ids that are in the runtime + * metadata but not in the static metadata. + * + * @param staticPackageToFunctionMap A map of package names to a set of function ids from the + * static metadata. + * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the + * runtime metadata. + * @return A map of package names to a set of function ids that are in the runtime metadata but + * not in the static metadata. + */ + @NonNull + @VisibleForTesting + static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap, + ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) { + return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap); + } + + @NonNull + private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap( + ArrayMap<String, ArraySet<String>> packageToFunctionMapA, + ArrayMap<String, ArraySet<String>> packageToFunctionMapB) { + ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>(); + for (String packageName : packageToFunctionMapA.keySet()) { + if (!packageToFunctionMapB.containsKey(packageName)) { + diffMap.put(packageName, packageToFunctionMapA.get(packageName)); + continue; + } + ArraySet<String> diffFunctions = new ArraySet<>(); + for (String functionId : + Objects.requireNonNull(packageToFunctionMapA.get(packageName))) { + if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName)) + .contains(functionId)) { + diffFunctions.add(functionId); + } + } + if (!diffFunctions.isEmpty()) { + diffMap.put(packageName, diffFunctions); + } + } + return diffMap; + } + + /** + * This method returns a map of package names to a set of function ids. + * + * @param queryExpression The query expression to use when searching for AppFunction metadata. + * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata. + * @return A map of package names to a set of function ids. + * @throws ExecutionException If the future search results fail to execute. + * @throws InterruptedException If the future search results are interrupted. + */ + @NonNull + @VisibleForTesting + @WorkerThread + ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap( + @NonNull String queryExpression, + @NonNull SearchSpec metadataSearchSpec, + @NonNull String propertyFunctionId, + @NonNull String propertyPackageName) + throws ExecutionException, InterruptedException { + ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>(); + FutureSearchResults futureSearchResults = + mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get(); + List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get(); + // TODO(b/357551503): This could be expensive if we have more functions + while (!searchResultsList.isEmpty()) { + for (SearchResult searchResult : searchResultsList) { + String packageName = + searchResult.getGenericDocument().getPropertyString(propertyPackageName); + String functionId = + searchResult.getGenericDocument().getPropertyString(propertyFunctionId); + packageToFunctionIds + .computeIfAbsent(packageName, k -> new ArraySet<>()) + .add(functionId); + } + searchResultsList = futureSearchResults.getNextPage().get(); + } + return packageToFunctionIds; + } +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java index 98903ae57a39..58597c38bb94 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java @@ -25,7 +25,6 @@ import android.os.UserHandle; * services are properly unbound after the operation completes or a timeout occurs. * * @param <T> Class of wrapped service. - * @hide */ public interface RemoteServiceCaller<T> { diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java index 0e18705c40b0..eea17eeca371 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java @@ -34,7 +34,6 @@ import java.util.function.Function; * Context#bindService}. * * @param <T> Class of wrapped service. - * @hide */ public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> { private static final String TAG = "AppFunctionsServiceCall"; diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 955b75d8bba0..3f4902db70f5 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -1122,7 +1122,11 @@ public final class BatteryStatsService extends IBatteryStats.Stub throw new UnsupportedOperationException("Unknown tagId=" + atomTag); } final byte[] statsProto = bus.getStatsProto(); - + try { + bus.close(); + } catch (IOException e) { + Slog.w(TAG, "Failure close BatteryUsageStats", e); + } data.add(FrameworkStatsLog.buildStatsEvent(atomTag, statsProto)); return StatsManager.PULL_SUCCESS; diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 8f52f67ff7e0..416c11090515 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -86,7 +86,6 @@ import com.android.server.ServiceThread; import dalvik.annotation.optimization.NeverCompile; -import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -2318,6 +2317,7 @@ public final class CachedAppOptimizer { Slog.d(TAG_AM, "Skipping freeze because process is marked " + "should not be frozen"); } + reportProcessFreezableChangedLocked(proc); return; } diff --git a/services/core/java/com/android/server/am/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 bd0024169324..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); diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index ac3c02823d0a..b2c616ae5b3c 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -316,6 +316,7 @@ class PreAuthInfo { Pair<BiometricSensor, Integer> sensorNotEnrolled = null; Pair<BiometricSensor, Integer> sensorLockout = null; Pair<BiometricSensor, Integer> hardwareNotDetected = null; + Pair<BiometricSensor, Integer> biometricAppNotAllowed = null; for (Pair<BiometricSensor, Integer> pair : ineligibleSensors) { final int status = pair.second; if (status == BIOMETRIC_LOCKOUT_TIMED || status == BIOMETRIC_LOCKOUT_PERMANENT) { @@ -327,6 +328,9 @@ class PreAuthInfo { if (status == BIOMETRIC_HARDWARE_NOT_DETECTED) { hardwareNotDetected = pair; } + if (status == BIOMETRIC_NOT_ENABLED_FOR_APPS) { + biometricAppNotAllowed = pair; + } } // If there is a sensor locked out, prioritize lockout over other sensor's error. @@ -339,6 +343,10 @@ class PreAuthInfo { return hardwareNotDetected; } + if (Flags.mandatoryBiometrics() && biometricAppNotAllowed != null) { + return biometricAppNotAllowed; + } + // If the caller requested STRONG, and the device contains both STRONG and non-STRONG // sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's // BIOMETRIC_INSUFFICIENT_STRENGTH error. diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 871121472938..407ef1e41aa6 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -321,6 +321,9 @@ public class Utils { case BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE: biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; break; + case BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS: + biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + break; default: Slog.e(BiometricService.TAG, "Unhandled result code: " + biometricConstantsCode); biometricManagerCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE; @@ -384,9 +387,12 @@ public class Utils { return BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED; case MANDATORY_BIOMETRIC_UNAVAILABLE_ERROR: return BiometricConstants.BIOMETRIC_ERROR_MANDATORY_NOT_ACTIVE; + case BIOMETRIC_NOT_ENABLED_FOR_APPS: + if (Flags.mandatoryBiometrics()) { + return BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; + } case BIOMETRIC_DISABLED_BY_DEVICE_POLICY: case BIOMETRIC_HARDWARE_NOT_DETECTED: - case BIOMETRIC_NOT_ENABLED_FOR_APPS: default: return BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE; } diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 17835b2d085b..ec61d4d39aa0 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -379,9 +379,7 @@ public class CameraServiceProxy extends SystemService streamCount = mStreamStats.size(); } if (CameraServiceProxy.DEBUG) { - String ultrawideDebug = Flags.logUltrawideUsage() - ? ", wideAngleUsage " + mUsedUltraWide - : ""; + String ultrawideDebug = ", wideAngleUsage " + mUsedUltraWide; String zoomOverrideDebug = Flags.logZoomOverrideUsage() ? ", zoomOverrideUsage " + mUsedZoomOverride : ""; @@ -1338,7 +1336,7 @@ public class CameraServiceProxy extends SystemService List<CameraStreamStats> streamStats = cameraState.getStreamStats(); String userTag = cameraState.getUserTag(); int videoStabilizationMode = cameraState.getVideoStabilizationMode(); - boolean usedUltraWide = Flags.logUltrawideUsage() ? cameraState.getUsedUltraWide() : false; + boolean usedUltraWide = cameraState.getUsedUltraWide(); boolean usedZoomOverride = Flags.logZoomOverrideUsage() ? cameraState.getUsedZoomOverride() : false; long logId = cameraState.getLogId(); diff --git a/services/core/java/com/android/server/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 38ef5b8cedb9..7d44ba199119 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -889,8 +889,14 @@ public class LockSettingsService extends ILockSettings.Stub { // Hide notification first, as tie profile lock takes time hideEncryptionNotification(new UserHandle(userId)); - if (isCredentialSharableWithParent(userId)) { - tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) { + synchronized (mSpManager) { + tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + } + } else { + if (isCredentialSharableWithParent(userId)) { + tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); + } } } }); @@ -1287,7 +1293,13 @@ public class LockSettingsService extends ILockSettings.Stub { mStorage.removeChildProfileLock(userId); removeKeystoreProfileKey(userId); } else { - tieProfileLockIfNecessary(userId, profileUserPassword); + if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) { + synchronized (mSpManager) { + tieProfileLockIfNecessary(userId, profileUserPassword); + } + } else { + tieProfileLockIfNecessary(userId, profileUserPassword); + } } } catch (IllegalStateException e) { setBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, old, userId); diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index bad959af7aad..925ba1752fe2 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -22,6 +22,7 @@ import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static com.android.server.notification.ZenLog.traceApplyDeviceEffect; import static com.android.server.notification.ZenLog.traceScheduleApplyDeviceEffect; +import android.app.KeyguardManager; import android.app.UiModeManager; import android.app.WallpaperManager; import android.content.BroadcastReceiver; @@ -53,6 +54,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { private final Context mContext; private final ColorDisplayManager mColorDisplayManager; + private final KeyguardManager mKeyguardManager; private final PowerManager mPowerManager; private final UiModeManager mUiModeManager; private final WallpaperManager mWallpaperManager; @@ -67,6 +69,7 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { DefaultDeviceEffectsApplier(Context context) { mContext = context; mColorDisplayManager = context.getSystemService(ColorDisplayManager.class); + mKeyguardManager = context.getSystemService(KeyguardManager.class); mPowerManager = context.getSystemService(PowerManager.class); mUiModeManager = context.getSystemService(UiModeManager.class); WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class); @@ -133,12 +136,14 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { // Changing the theme can be disruptive for the user (Activities are likely recreated, may // lose some state). Therefore we only apply the change immediately if the rule was - // activated manually, or we are initializing, or the screen is currently off/dreaming. + // activated manually, or we are initializing, or the screen is currently off/dreaming, + // or if the device is locked. if (origin == ZenModeConfig.ORIGIN_INIT || origin == ZenModeConfig.ORIGIN_INIT_USER || origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI || origin == ZenModeConfig.ORIGIN_USER_IN_APP - || !mPowerManager.isInteractive()) { + || !mPowerManager.isInteractive() + || (android.app.Flags.modesUi() && mKeyguardManager.isKeyguardLocked())) { unregisterScreenOffReceiver(); updateNightModeImmediately(useNightMode); } else { diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 3523a3336a63..03fc60cad8d6 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -1094,7 +1094,7 @@ abstract public class ManagedServices { return info; } throw new SecurityException("Disallowed call from unknown " + getCaption() + ": " - + service + " " + service.getClass()); + + service.asBinder() + " " + service.getClass()); } public boolean isSameUser(IInterface service, int userId) { @@ -1585,6 +1585,9 @@ abstract public class ManagedServices { // after the rebind delay if (isPackageOrComponentAllowedWithPermission(cn, userId)) { registerService(cn, userId); + } else { + if (DEBUG) Slog.v(TAG, "skipped reregisterService cn=" + cn + " u=" + userId + + " because of isPackageOrComponentAllowedWithPermission check"); } } @@ -1918,6 +1921,7 @@ abstract public class ManagedServices { .append(",targetSdkVersion=").append(targetSdkVersion) .append(",connection=").append(connection == null ? null : "<connection>") .append(",service=").append(service) + .append(",serviceAsBinder=").append(service != null ? service.asBinder() : null) .append(']').toString(); } @@ -1956,7 +1960,7 @@ abstract public class ManagedServices { @Override public void binderDied() { - if (DEBUG) Slog.d(TAG, "binderDied"); + if (DEBUG) Slog.d(TAG, "binderDied " + this); // Remove the service, but don't unbind from the service. The system will bring the // service back up, and the onServiceConnected handler will read the service with the // new binding. If this isn't a bound service, and is just a registered diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 43a285cba4b9..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/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 5e45b4c2d5af..f53234215fc6 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -936,13 +936,9 @@ public class PackageArchiver { * <p> In the rare case the app had multiple launcher activities, only one of the icons is * returned arbitrarily. * - * <p> By default, the icon will be overlay'd with a cloud icon on top. A launcher app can + * <p> By default, the icon will be overlay'd with a cloud icon on top. An app can * disable the cloud overlay via the * {@link LauncherApps.ArchiveCompatibilityParams#setEnableIconOverlay(boolean)} API. - * The default launcher's cloud overlay mode determines the cloud overlay status returned by - * any other callers. That is, if the current launcher has the cloud overlay disabled, any other - * app that fetches the app icon will also get an icon that has the cloud overlay disabled. - * This is to prevent style mismatch caused by icons that are fetched by different callers. */ @Nullable public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9fb9e717fe4d..9428de700385 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -925,6 +925,18 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return PackageArchiver.isArchived(readUserState(userId)); } + /** + * @return if the package is archived in any of the users + */ + boolean isArchivedOnAnyUser(int[] userIds) { + for (int user : userIds) { + if (isArchived(user)) { + return true; + } + } + return false; + } + int getInstallReason(int userId) { return readUserState(userId).getInstallReason(); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 1fcd7f1ef861..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; @@ -2220,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); @@ -5521,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); } @@ -5688,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(); @@ -5748,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; @@ -6255,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/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index f53dda6ee35b..4dcc6e112ecc 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3169,7 +3169,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final WallpaperDestinationChangeHandler liveSync = new WallpaperDestinationChangeHandler( newWallpaper); - boolean same = changingToSame(name, newWallpaper); + boolean same = changingToSame(name, newWallpaper.connection, + newWallpaper.wallpaperComponent); /* * If we have a shared system+lock wallpaper, and we reapply the same wallpaper @@ -3257,14 +3258,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return name == null || name.equals(mDefaultWallpaperComponent); } - private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) { - if (wallpaper.connection != null) { - final ComponentName wallpaperName = wallpaper.wallpaperComponent; - if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) { + private boolean changingToSame(ComponentName newComponentName, + WallpaperConnection currentConnection, ComponentName currentComponentName) { + if (currentConnection != null) { + if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) { if (DEBUG) Slog.v(TAG, "changingToSame: still using default"); // Still using default wallpaper. return true; - } else if (wallpaperName != null && wallpaperName.equals(componentName)) { + } else if (currentComponentName != null && currentComponentName.equals( + newComponentName)) { // Changing to same wallpaper. if (DEBUG) Slog.v(TAG, "same wallpaper"); return true; @@ -3279,7 +3281,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName); } // Has the component changed? - if (!force && changingToSame(componentName, wallpaper)) { + if (!force && changingToSame(componentName, wallpaper.connection, + wallpaper.wallpaperComponent)) { try { if (DEBUG_LIVE) { Slog.v(TAG, "Changing to the same component, ignoring"); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 99747e05e7f0..0be6471f189e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8149,7 +8149,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ @Override protected int getOverrideOrientation() { - if (mWmService.mConstants.mIgnoreActivityOrientationRequest) { + if (mWmService.mConstants.mIgnoreActivityOrientationRequest + && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) { return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } return mAppCompatController.getOrientationPolicy() diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index b8ce02ed5937..3d6b64b2e536 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -130,35 +130,6 @@ public abstract class ActivityTaskManagerInternal { } /** - * Sleep tokens cause the activity manager to put the top activity to sleep. - * They are used by components such as dreams that may hide and block interaction - * with underlying activities. - * The Acquirer provides an interface that encapsulates the underlying work, so the user does - * not need to handle the token by him/herself. - */ - public interface SleepTokenAcquirer { - - /** - * Acquires a sleep token. - * @param displayId The display to apply to. - */ - void acquire(int displayId); - - /** - * Releases the sleep token. - * @param displayId The display to apply to. - */ - void release(int displayId); - } - - /** - * Creates a sleep token acquirer for the specified display with the specified tag. - * - * @param tag A string identifying the purpose (eg. "Dream"). - */ - public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag); - - /** * Returns home activity for the specified user. * * @param userId ID of the user or {@link android.os.UserHandle#USER_ALL} diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index e25d940d9781..49ca698e36e2 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -4356,6 +4356,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mTaskOrganizerController.dump(pw, " "); mVisibleActivityProcessTracker.dump(pw, " "); mActiveUids.dump(pw, " "); + pw.println(" SleepTokens=" + mRootWindowContainer.mSleepTokens); if (mDemoteTopAppReasons != 0) { pw.println(" mDemoteTopAppReasons=" + mDemoteTopAppReasons); } @@ -5071,17 +5072,16 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason); } - final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer { + final class SleepTokenAcquirer { private final String mTag; private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens = new SparseArray<>(); - SleepTokenAcquirerImpl(@NonNull String tag) { + SleepTokenAcquirer(@NonNull String tag) { mTag = tag; } - @Override - public void acquire(int displayId) { + void acquire(int displayId) { synchronized (mGlobalLock) { if (!mSleepTokens.contains(displayId)) { mSleepTokens.append(displayId, @@ -5091,8 +5091,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } - @Override - public void release(int displayId) { + void release(int displayId) { synchronized (mGlobalLock) { final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId); if (token != null) { @@ -5955,11 +5954,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } final class LocalService extends ActivityTaskManagerInternal { - @Override - public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) { - Objects.requireNonNull(tag); - return new SleepTokenAcquirerImpl(tag); - } @Override public ComponentName getHomeActivityForUser(int userId) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e8a3951a93d4..10e0641b0582 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -735,8 +735,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** All tokens used to put activities on this root task to sleep (including mOffToken) */ final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>(); - /** The token acquirer to put root tasks on the display to sleep */ - private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer; private boolean mSleeping; @@ -1131,7 +1129,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplay = display; mDisplayId = display.getDisplayId(); mCurrentUniqueDisplayId = display.getUniqueId(); - mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer; mWallpaperController = new WallpaperController(mWmService, this); mWallpaperController.resetLargestDisplay(display); display.getDisplayInfo(mDisplayInfo); @@ -6157,9 +6154,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp final int displayState = mDisplayInfo.state; if (displayId != DEFAULT_DISPLAY) { if (displayState == Display.STATE_OFF) { - mOffTokenAcquirer.acquire(mDisplayId); + mRootWindowContainer.mDisplayOffTokenAcquirer.acquire(mDisplayId); } else if (displayState == Display.STATE_ON) { - mOffTokenAcquirer.release(mDisplayId); + mRootWindowContainer.mDisplayOffTokenAcquirer.release(mDisplayId); } ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: Display %d state was (%d), is now (%d), so update " diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 5c621208c4db..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/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt index 5233f194d6c5..a0f1a559bb52 100644 --- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt @@ -16,6 +16,7 @@ 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 @@ -42,7 +43,7 @@ class FutureAppSearchSessionTest { fun clearData() { val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build() FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { - val setSchemaRequest = SetSchemaRequest.Builder().build() + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() it.setSchema(setSchemaRequest) } } @@ -123,6 +124,38 @@ class FutureAppSearchSessionTest { } } + @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" diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt new file mode 100644 index 000000000000..1fa55c7090aa --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema +import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema +import android.app.appsearch.AppSearchManager +import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SetSchemaRequest +import android.app.appsearch.observer.DocumentChangeInfo +import android.app.appsearch.observer.ObserverCallback +import android.app.appsearch.observer.ObserverSpec +import android.app.appsearch.observer.SchemaChangeInfo +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.infra.AndroidFuture +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class FutureGlobalSearchSessionTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun registerDocumentChangeObserverCallback() { + val packageObserverSpec: ObserverSpec = + ObserverSpec.Builder() + .addFilterSchemas( + AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(TEST_TARGET_PKG_NAME) + ) + .build() + val settableDocumentChangeInfo: AndroidFuture<DocumentChangeInfo> = AndroidFuture() + val observer: ObserverCallback = + object : ObserverCallback { + override fun onSchemaChanged(changeInfo: SchemaChangeInfo) {} + + override fun onDocumentChanged(changeInfo: DocumentChangeInfo) { + settableDocumentChangeInfo.complete(changeInfo) + } + } + val futureGlobalSearchSession = FutureGlobalSearchSession(appSearchManager, testExecutor) + + val registerPackageObserver: Void? = + futureGlobalSearchSession + .registerObserverCallbackAsync( + TEST_TARGET_PKG_NAME, + packageObserverSpec, + testExecutor, + observer, + ) + .get() + assertThat(registerPackageObserver).isNull() + // Trigger document change + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session -> + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas( + createParentAppFunctionRuntimeSchema(), + createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME), + ) + .build() + val schema = session.setSchema(setSchemaRequest) + assertThat(schema.get()).isNotNull() + val appFunctionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, TEST_FUNCTION_ID, "") + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments(appFunctionRuntimeMetadata) + .build() + val putResult = session.put(putDocumentsRequest).get() + assertThat(putResult.isSuccess).isTrue() + } + assertThat( + settableDocumentChangeInfo + .get() + .changedDocumentIds + .contains( + AppFunctionRuntimeMetadata.getDocumentIdForAppFunction( + TEST_TARGET_PKG_NAME, + TEST_FUNCTION_ID, + ) + ) + ) + .isTrue() + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" + const val TEST_FUNCTION_ID: String = "print" + } +} diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt new file mode 100644 index 000000000000..1061da28f799 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.appfunctions + +import android.app.appfunctions.AppFunctionRuntimeMetadata +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID +import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME +import android.app.appsearch.AppSearchManager +import android.app.appsearch.AppSearchManager.SearchContext +import android.app.appsearch.PutDocumentsRequest +import android.app.appsearch.SearchSpec +import android.app.appsearch.SetSchemaRequest +import android.util.ArrayMap +import android.util.ArraySet +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class MetadataSyncAdapterTest { + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val appSearchManager = context.getSystemService(AppSearchManager::class.java) + private val testExecutor = MoreExecutors.directExecutor() + + @Before + @After + fun clearData() { + val searchContext = SearchContext.Builder(TEST_DB).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build() + it.setSchema(setSchemaRequest) + } + } + + @Test + fun getPackageToFunctionIdMap() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId") + } + + @Test + fun getPackageToFunctionIdMap_multipleDocuments() { + val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build() + val functionRuntimeMetadata = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build() + val functionRuntimeMetadata1 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build() + val functionRuntimeMetadata2 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build() + val functionRuntimeMetadata3 = + AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build() + val setSchemaRequest = + SetSchemaRequest.Builder() + .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema()) + .addSchemas( + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + ) + .build() + val putDocumentsRequest: PutDocumentsRequest = + PutDocumentsRequest.Builder() + .addGenericDocuments( + functionRuntimeMetadata, + functionRuntimeMetadata1, + functionRuntimeMetadata2, + functionRuntimeMetadata3, + ) + .build() + FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { + val setSchemaResponse = it.setSchema(setSchemaRequest).get() + assertThat(setSchemaResponse).isNotNull() + val appSearchBatchResult = it.put(putDocumentsRequest).get() + assertThat(appSearchBatchResult.isSuccess).isTrue() + } + + val metadataSyncAdapter = + MetadataSyncAdapter( + testExecutor, + FutureAppSearchSession(appSearchManager, testExecutor, searchContext), + ) + val searchSpec: SearchSpec = + SearchSpec.Builder() + .setResultCountPerPage(1) + .addFilterSchemas( + AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE, + AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME) + .schemaType, + ) + .build() + val packageToFunctionIdMap = + metadataSyncAdapter.getPackageToFunctionIdMap( + "", + searchSpec, + PROPERTY_FUNCTION_ID, + PROPERTY_PACKAGE_NAME, + ) + + assertThat(packageToFunctionIdMap).isNotNull() + assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]) + .containsExactly( + "testFunctionId", + "testFunctionId1", + "testFunctionId2", + "testFunctionId3", + ) + } + + @Test + fun getAddedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getAddedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2") + } + + @Test + fun getAddedFunctionsDiffMap_addedFunctionNewPackage() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.size).isEqualTo(1) + assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getAddedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val addedFunctionsDiffMap = + MetadataSyncAdapter.getAddedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_noDiff() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = + ArrayMap(staticPackageToFunctionMap) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + @Test + fun getRemovedFunctionsDiffMap_removedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + runtimePackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.size).isEqualTo(1) + assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1") + } + + @Test + fun getRemovedFunctionsDiffMap_addedFunction() { + val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + staticPackageToFunctionMap.putAll( + mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1"))) + ) + val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap() + + val removedFunctionsDiffMap = + MetadataSyncAdapter.getRemovedFunctionsDiffMap( + staticPackageToFunctionMap, + runtimePackageToFunctionMap, + ) + + assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true) + } + + private companion object { + const val TEST_DB: String = "test_db" + const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests" + } +} diff --git a/services/tests/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/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 6b8e414255cd..b4b36125f770 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -558,7 +558,9 @@ public class BiometricServiceTest { waitForIdle(); verify(mReceiver1).onError( eq(BiometricAuthenticator.TYPE_NONE), - eq(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), + eq(Flags.mandatoryBiometrics() + ? BiometricConstants.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS + : BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE), eq(0 /* vendorCode */)); // Enrolled, not disabled in settings, user requires confirmation in settings @@ -1450,7 +1452,9 @@ public class BiometricServiceTest { } @Test - public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception { + @RequiresFlagsDisabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCanAuthenticate_whenBiometricsNotEnabledForApps_returnsHardwareUnavailable() + throws Exception { setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) @@ -1468,6 +1472,25 @@ public class BiometricServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception { + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); + when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) + .thenReturn(true); + + // When only biometric is requested + int authenticators = Authenticators.BIOMETRIC_STRONG; + assertEquals(BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS, + invokeCanAuthenticate(mBiometricService, authenticators)); + + // When credential and biometric are requested + authenticators = Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL; + assertEquals(BiometricManager.BIOMETRIC_SUCCESS, + invokeCanAuthenticate(mBiometricService, authenticators)); + } + + @Test public void testCanAuthenticate_whenNoBiometricSensor() throws Exception { mBiometricService = new BiometricService(mContext, mInjector, mBiometricHandlerProvider); mBiometricService.onStart(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java index 760d38e855a6..b758f57ff407 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -20,6 +20,7 @@ import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NO import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS; import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE; @@ -266,6 +267,29 @@ public class PreAuthInfoTest { @Test @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testCalculateByPriority() + throws Exception { + when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false); + when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); + + BiometricSensor faceSensor = getFaceSensor(); + BiometricSensor fingerprintSensor = getFingerprintSensor(); + PromptInfo promptInfo = new PromptInfo(); + promptInfo.setConfirmationRequested(false /* requireConfirmation */); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); + PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(faceSensor, fingerprintSensor), + 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + + assertThat(preAuthInfo.eligibleSensors).hasSize(0); + assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo( + BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) public void testMandatoryBiometricsNegativeButtonText_whenSet() throws Exception { when(mTrustManager.isInSignificantPlace()).thenReturn(false); diff --git a/services/tests/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/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index 112471b2af57..c85374e0b660 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -376,7 +376,8 @@ interface ITelecomService { */ void requestLogMark(in String message); - void setTestPhoneAcctSuggestionComponent(String flattenedComponentName); + void setTestPhoneAcctSuggestionComponent(String flattenedComponentName, + in UserHandle userHandle); void setTestDefaultCallScreeningApp(String packageName); diff --git a/telephony/java/android/telephony/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/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 189de6bdb44a..e841d9ea0880 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -399,16 +399,12 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - var assertion = assertThrows(RuntimeException.class, () -> implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, - new Object[]{5})); + implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + new Object[]{5}); - verify(implSpy, never()).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), any()); + verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( + LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)")); verify(sReader).getViewerString(eq(1234L)); - - Truth.assertThat(assertion).hasMessageThat() - .contains("Failed to get log message with hash 1234 and args (5)"); } @Test @@ -866,19 +862,6 @@ 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/aapt/Package.cpp b/tools/aapt/Package.cpp index 5e0f87f0dcaf..60c4bf5c4131 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -292,13 +292,12 @@ bool processFile(Bundle* bundle, ZipFile* zip, } if (!hasData) { const String8& srcName = file->getSourceFile(); - time_t fileModWhen; - fileModWhen = getFileModDate(srcName.c_str()); - if (fileModWhen == (time_t) -1) { // file existence tested earlier, - return false; // not expecting an error here + auto fileModWhen = getFileModDate(srcName.c_str()); + if (fileModWhen == kInvalidModDate) { // file existence tested earlier, + return false; // not expecting an error here } - - if (fileModWhen > entry->getModWhen()) { + + if (toTimeT(fileModWhen) > entry->getModWhen()) { // mark as deleted so add() will succeed if (bundle->getVerbose()) { printf(" (removing old '%s')\n", storageName.c_str()); diff --git a/tools/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 |