diff options
263 files changed, 8319 insertions, 2485 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index e20f525fcdaf..e489c1ad891a 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -38,3 +38,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "thermal_restrictions_to_fgs_jobs" + namespace: "backstage_power" + description: "Apply thermal restrictions to FGS jobs." + bug: "315157163" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 3bb395f39123..ba8e3e8b48fc 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -1375,8 +1375,10 @@ class JobConcurrencyManager { final JobServiceContext jsc = mActiveServices.get(i); final JobStatus jobStatus = jsc.getRunningJobLocked(); - if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime() - && restriction.isJobRestricted(jobStatus)) { + if (jobStatus != null + && !jsc.isWithinExecutionGuaranteeTime() + && restriction.isJobRestricted( + jobStatus, mService.evaluateJobBiasLocked(jobStatus))) { jsc.cancelExecutingJobLocked(restriction.getStopReason(), restriction.getInternalReason(), JobParameters.getInternalReasonCodeDescription( diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index 5d1433c815d6..384d78618c8b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -310,7 +310,8 @@ public class JobSchedulerService extends com.android.server.SystemService * Note: do not add to or remove from this list at runtime except in the constructor, because we * do not synchronize access to this list. */ - private final List<JobRestriction> mJobRestrictions; + @VisibleForTesting + final List<JobRestriction> mJobRestrictions; @GuardedBy("mLock") @VisibleForTesting @@ -3498,8 +3499,6 @@ public class JobSchedulerService extends com.android.server.SystemService /** * Check if a job is restricted by any of the declared {@link JobRestriction JobRestrictions}. - * Note, that the jobs with {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher may not - * be restricted, thus we won't even perform the check, but simply return null early. * * @param job to be checked * @return the first {@link JobRestriction} restricting the given job that has been found; null @@ -3508,13 +3507,9 @@ public class JobSchedulerService extends com.android.server.SystemService */ @GuardedBy("mLock") JobRestriction checkIfRestricted(JobStatus job) { - if (evaluateJobBiasLocked(job) >= JobInfo.BIAS_FOREGROUND_SERVICE) { - // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted - return null; - } for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { final JobRestriction restriction = mJobRestrictions.get(i); - if (restriction.isJobRestricted(job)) { + if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) { return restriction; } } @@ -4221,6 +4216,7 @@ public class JobSchedulerService extends com.android.server.SystemService return curBias; } + /** Gets and returns the adjusted Job Bias **/ int evaluateJobBiasLocked(JobStatus job) { int bias = job.getBias(); if (bias >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) { @@ -5907,7 +5903,7 @@ public class JobSchedulerService extends com.android.server.SystemService if (isRestricted) { for (int i = mJobRestrictions.size() - 1; i >= 0; i--) { final JobRestriction restriction = mJobRestrictions.get(i); - if (restriction.isJobRestricted(job)) { + if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) { final int reason = restriction.getInternalReason(); pw.print(" "); pw.print(JobParameters.getInternalReasonCodeDescription(reason)); @@ -6240,7 +6236,7 @@ public class JobSchedulerService extends com.android.server.SystemService proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON, restriction.getInternalReason()); proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING, - restriction.isJobRestricted(job)); + restriction.isJobRestricted(job, evaluateJobBiasLocked(job))); proto.end(restrictionsToken); } diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java index 7aab67a00b1d..555a1186e0c9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java @@ -62,10 +62,11 @@ public abstract class JobRestriction { * fine with it). * * @param job to be checked + * @param bias job bias to be checked * @return false if the {@link JobSchedulerService} should not schedule this job at the moment, * true - otherwise */ - public abstract boolean isJobRestricted(JobStatus job); + public abstract boolean isJobRestricted(JobStatus job, int bias); /** Dump any internal constants the Restriction may have. */ public abstract void dumpConstants(IndentingPrintWriter pw); diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java index ef634b565b65..ba0111349bc9 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -24,6 +24,7 @@ import android.os.PowerManager.OnThermalStatusChangedListener; import android.util.IndentingPrintWriter; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.job.Flags; import com.android.server.job.JobSchedulerService; import com.android.server.job.controllers.JobStatus; @@ -85,7 +86,18 @@ public class ThermalStatusRestriction extends JobRestriction { } @Override - public boolean isJobRestricted(JobStatus job) { + public boolean isJobRestricted(JobStatus job, int bias) { + if (Flags.thermalRestrictionsToFgsJobs()) { + if (bias >= JobInfo.BIAS_TOP_APP) { + // Jobs with BIAS_TOP_APP should not be restricted + return false; + } + } else { + if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) { + // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted + return false; + } + } if (mThermalStatus >= UPPER_THRESHOLD) { return true; } @@ -107,6 +119,17 @@ public class ThermalStatusRestriction extends JobRestriction { || (mService.isCurrentlyRunningLocked(job) && mService.isJobInOvertimeLocked(job)); } + if (Flags.thermalRestrictionsToFgsJobs()) { + // Only let foreground jobs run if: + // 1. They haven't previously run + // 2. They're already running and aren't yet in overtime + if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE + && job.getJob().isImportantWhileForeground()) { + return job.getNumPreviousAttempts() > 0 + || (mService.isCurrentlyRunningLocked(job) + && mService.isJobInOvertimeLocked(job)); + } + } if (priority == JobInfo.PRIORITY_HIGH) { return !mService.isCurrentlyRunningLocked(job) || mService.isJobInOvertimeLocked(job); @@ -114,6 +137,13 @@ public class ThermalStatusRestriction extends JobRestriction { return true; } if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) { + if (Flags.thermalRestrictionsToFgsJobs()) { + if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) { + // No restrictions on foreground jobs + // on LOW_PRIORITY_THRESHOLD and below + return false; + } + } // For light throttling, throttle all min priority jobs and all low priority jobs that // aren't already running or have been running for long enough. return priority == JobInfo.PRIORITY_MIN diff --git a/cmds/bootanimation/FORMAT.md b/cmds/bootanimation/FORMAT.md index 01e8fe13fdf6..da8331af1492 100644 --- a/cmds/bootanimation/FORMAT.md +++ b/cmds/bootanimation/FORMAT.md @@ -126,7 +126,7 @@ the system property `service.bootanim.exit` to a nonzero string.) Use `zopflipng` if you have it, otherwise `pngcrush` will do. e.g.: for fn in *.png ; do - zopflipng -m ${fn}s ${fn}s.new && mv -f ${fn}s.new ${fn} + zopflipng -m ${fn} ${fn}.new && mv -f ${fn}.new ${fn} # or: pngcrush -q .... done diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java index be8f48df4aa6..c8ab260c8c4e 100644 --- a/core/java/android/app/ActivityTaskManager.java +++ b/core/java/android/app/ActivityTaskManager.java @@ -90,13 +90,6 @@ public class ActivityTaskManager { public static final int RESIZE_MODE_USER = RESIZE_MODE_PRESERVE_WINDOW; /** - * Input parameter to {@link IActivityTaskManager#resizeTask} used by window - * manager during a screen rotation. - * @hide - */ - public static final int RESIZE_MODE_SYSTEM_SCREEN_ROTATION = RESIZE_MODE_PRESERVE_WINDOW; - - /** * Input parameter to {@link IActivityTaskManager#resizeTask} which indicates * that the resize should be performed even if the bounds appears unchanged. * @hide diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index c3bac710f9be..4ac6bac3a2e5 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -6490,7 +6490,8 @@ public class Notification implements Parcelable // visual regressions. @SuppressWarnings("AndroidFrameworkCompatChange") private boolean bigContentViewRequired() { - if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) { + if (!Flags.notificationExpansionOptional() + && mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) { return true; } // Notifications with contentView and without a bigContentView, style, or actions would @@ -6593,12 +6594,7 @@ public class Notification implements Parcelable * @hide */ public RemoteViews createCompactHeadsUpContentView() { - // TODO(b/336225281): re-evaluate custom view usage. - if (useExistingRemoteView(mN.headsUpContentView)) { - return fullyCustomViewRequiresDecoration(false /* fromStyle */) - ? minimallyDecoratedHeadsUpContentView(mN.headsUpContentView) - : mN.headsUpContentView; - } else if (mStyle != null) { + if (mStyle != null) { final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView(); if (styleView != null) { return styleView; @@ -6611,7 +6607,7 @@ public class Notification implements Parcelable // Notification text is shown as secondary header text // for the minimal hun when it is provided. // Time(when and chronometer) is not shown for the minimal hun. - p.headerTextSecondary(p.mText).text(null).hideTime(true); + p.headerTextSecondary(p.mText).text(null).hideTime(true).summaryText(""); return applyStandardTemplate( getCompactHeadsUpBaseLayoutResource(), p, diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 63ffaa0f0630..50c7b7f9798e 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -60,6 +60,13 @@ flag { } flag { + name: "notification_expansion_optional" + namespace: "systemui" + description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions." + bug: "339523906" +} + +flag { name: "keyguard_private_notifications" namespace: "systemui" description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()" diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index c7b168aaf81d..04c36867271c 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -47,3 +47,14 @@ flag { description: "Feature flag for collecting app data size by file type API" bug: "294088945" } + +flag { + name: "disable_idle_check" + namespace: "backstage_power" + description: "disable idle check for USER_SYSTEM during boot up" + is_fixed_read_only: true + bug: "337864590" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 45591d79ee00..cee8d96fb0d7 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -137,6 +137,18 @@ flag { } flag { + name: "get_package_storage_stats" + namespace: "system_performance" + is_exported: true + description: "Add dumpsys entry point for package StorageStats" + bug: "332905331" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "provide_info_of_apk_in_apex" is_exported: true namespace: "package_manager_service" diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index cd1913bb26d9..83742eb7ae84 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -240,3 +240,10 @@ flag { description: "Add entrypoint in Settings Reset options for deleting private space when lock is forgotten" bug: "329601751" } + +flag { + name: "allow_main_user_to_access_blocked_number_provider" + namespace: "multiuser" + description: "Allow MAIN user to access blocked number provider" + bug: "338579331" +} diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig index 9fe0befb4f97..d4d1ed22dd4e 100644 --- a/core/java/android/net/vcn/flags.aconfig +++ b/core/java/android/net/vcn/flags.aconfig @@ -38,6 +38,16 @@ flag{ } flag{ + name: "enforce_main_user" + namespace: "vcn" + description: "Enforce main user to make VCN HSUM compatible" + bug: "310310661" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag{ name: "handle_seq_num_leap" namespace: "vcn" description: "Do not report bad network when there is a suspected sequence number leap" diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java index 4530157d2fe1..9503f4925e14 100644 --- a/core/java/android/view/ImeBackAnimationController.java +++ b/core/java/android/view/ImeBackAnimationController.java @@ -38,6 +38,8 @@ import android.window.OnBackAnimationCallback; import com.android.internal.inputmethod.SoftInputShowHideReason; +import java.io.PrintWriter; + /** * Controller for IME predictive back animation * @@ -271,4 +273,24 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { return mPostCommitAnimator != null && mTriggerBack; } + /** + * Dump information about this ImeBackAnimationController + * + * @param prefix the prefix that will be prepended to each line of the produced output + * @param writer the writer that will receive the resulting text + */ + public void dump(String prefix, PrintWriter writer) { + final String innerPrefix = prefix + " "; + writer.println(prefix + "ImeBackAnimationController:"); + writer.println(innerPrefix + "mLastProgress=" + mLastProgress); + writer.println(innerPrefix + "mTriggerBack=" + mTriggerBack); + writer.println(innerPrefix + "mIsPreCommitAnimationInProgress=" + + mIsPreCommitAnimationInProgress); + writer.println(innerPrefix + "mStartRootScrollY=" + mStartRootScrollY); + writer.println(innerPrefix + "isBackAnimationAllowed=" + isBackAnimationAllowed()); + writer.println(innerPrefix + "isAdjustPan=" + isAdjustPan()); + writer.println(innerPrefix + "isHideAnimationInProgress=" + + isHideAnimationInProgress()); + } + } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index c54526747c5c..f1cb4103f008 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1799,8 +1799,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } void dump(String prefix, PrintWriter pw) { - pw.print(prefix); pw.println("InsetsController:"); - mState.dump(prefix + " ", pw); + final String innerPrefix = prefix + " "; + pw.println(prefix + "InsetsController:"); + mState.dump(innerPrefix, pw); + pw.println(innerPrefix + "mIsPredictiveBackImeHideAnimInProgress=" + + mIsPredictiveBackImeHideAnimInProgress); } void dumpDebug(ProtoOutputStream proto, long fieldId) { diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index a23df799da59..1d70d18ac4c8 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -986,7 +986,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall updateBackgroundVisibility(surfaceUpdateTransaction); updateBackgroundColor(surfaceUpdateTransaction); - if (mLimitedHdrEnabled && hdrHeadroomChanged) { + if (mLimitedHdrEnabled && (hdrHeadroomChanged || creating)) { surfaceUpdateTransaction.setDesiredHdrHeadroom( mBlastSurfaceControl, mHdrHeadroom); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index db55a9d03c9f..6de50f4e6fdf 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -423,6 +423,12 @@ public final class ViewRootImpl implements ViewParent, private static final long NANOS_PER_SEC = 1000000000; + // If the ViewRootImpl has been idle for more than 200ms, clear the preferred + // frame rate category and frame rate. + private static final int IDLE_TIME_MILLIS = 250; + + private static final long NANOS_PER_MILLI = 1_000_000; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>(); @@ -655,6 +661,8 @@ public final class ViewRootImpl implements ViewParent, private int mMinusOneFrameIntervalMillis = 0; // VRR interval between the previous and the frame before private int mMinusTwoFrameIntervalMillis = 0; + // VRR has the invalidation idle message been posted? + private boolean mInvalidationIdleMessagePosted = false; /** * Update the Choreographer's FrameInfo object with the timing information for the current @@ -4266,6 +4274,10 @@ public final class ViewRootImpl implements ViewParent, // when the values are applicable. if (mDrawnThisFrame) { mDrawnThisFrame = false; + if (!mInvalidationIdleMessagePosted) { + mInvalidationIdleMessagePosted = true; + mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS); + } setCategoryFromCategoryCounts(); updateInfrequentCount(); setPreferredFrameRate(mPreferredFrameRate); @@ -5300,10 +5312,12 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_CONTENT_CAPTURE) { Log.v(mTag, "performContentCaptureInitialReport() on " + rootView); } + boolean traceDispatchCapture = false; try { if (!isContentCaptureEnabled()) return; - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + traceDispatchCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); + if (traceDispatchCapture) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchContentCapture() for " + getClass().getSimpleName()); } @@ -5319,7 +5333,9 @@ public final class ViewRootImpl implements ViewParent, // Content capture is a go! rootView.dispatchInitialProvideContentCaptureStructure(); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); + if (traceDispatchCapture) { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } } @@ -5327,10 +5343,12 @@ public final class ViewRootImpl implements ViewParent, if (DEBUG_CONTENT_CAPTURE) { Log.v(mTag, "handleContentCaptureFlush()"); } + boolean traceFlushContentCapture = false; try { if (!isContentCaptureEnabled()) return; - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + traceFlushContentCapture = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); + if (traceFlushContentCapture) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "flushContentCapture for " + getClass().getSimpleName()); } @@ -5342,7 +5360,9 @@ public final class ViewRootImpl implements ViewParent, } ccm.flush(ContentCaptureSession.FLUSH_REASON_VIEW_ROOT_ENTERED); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); + if (traceFlushContentCapture) { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } } @@ -6499,6 +6519,8 @@ public final class ViewRootImpl implements ViewParent, return "MSG_WINDOW_TOUCH_MODE_CHANGED"; case MSG_KEEP_CLEAR_RECTS_CHANGED: return "MSG_KEEP_CLEAR_RECTS_CHANGED"; + case MSG_CHECK_INVALIDATION_IDLE: + return "MSG_CHECK_INVALIDATION_IDLE"; case MSG_REFRESH_POINTER_ICON: return "MSG_REFRESH_POINTER_ICON"; case MSG_TOUCH_BOOST_TIMEOUT: @@ -6763,6 +6785,30 @@ public final class ViewRootImpl implements ViewParent, mNumPausedForSync = 0; scheduleTraversals(); break; + case MSG_CHECK_INVALIDATION_IDLE: { + long delta; + if (mIsTouchBoosting || mIsFrameRateBoosting || mInsetsAnimationRunning) { + delta = 0; + } else { + delta = System.nanoTime() / NANOS_PER_MILLI - mLastUpdateTimeMillis; + } + if (delta >= IDLE_TIME_MILLIS) { + mFrameRateCategoryHighCount = 0; + mFrameRateCategoryHighHintCount = 0; + mFrameRateCategoryNormalCount = 0; + mFrameRateCategoryLowCount = 0; + mPreferredFrameRate = 0; + mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; + setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE); + setPreferredFrameRate(0f); + mInvalidationIdleMessagePosted = false; + } else { + mInvalidationIdleMessagePosted = true; + mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, + IDLE_TIME_MILLIS - delta); + } + break; + } case MSG_TOUCH_BOOST_TIMEOUT: /** * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). @@ -9601,6 +9647,8 @@ public final class ViewRootImpl implements ViewParent, mOnBackInvokedDispatcher.dump(prefix, writer); + mImeBackAnimationController.dump(prefix, writer); + writer.println(prefix + "View Hierarchy:"); dumpViewHierarchy(innerPrefix, writer, mView); } @@ -12657,10 +12705,12 @@ public final class ViewRootImpl implements ViewParent, view = mFrameRateCategoryView; } + boolean traceFrameRateCategory = false; try { if (frameRateCategory != FRAME_RATE_CATEGORY_DEFAULT && mLastPreferredFrameRateCategory != frameRateCategory) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + traceFrameRateCategory = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); + if (traceFrameRateCategory) { String reason = reasonToString(frameRateReason); String sourceView = view == null ? "-" : view; String category = categoryToString(frameRateCategory); @@ -12678,7 +12728,9 @@ public final class ViewRootImpl implements ViewParent, } catch (Exception e) { Log.e(mTag, "Unable to set frame rate category", e); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); + if (traceFrameRateCategory) { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } } @@ -13002,6 +13054,10 @@ public final class ViewRootImpl implements ViewParent, private void removeVrrMessages() { mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT); mHandler.removeMessages(MSG_FRAME_RATE_SETTING); + if (mInvalidationIdleMessagePosted) { + mInvalidationIdleMessagePosted = false; + mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE); + } } /** diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index a2f3544c70ab..5aaa994f3f8f 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -374,7 +374,7 @@ public class AnimationSet extends Animation { final Animation a = mAnimations.get(i); temp.clear(); - a.getTransformationAt(interpolatedTime, t); + a.getTransformationAt(interpolatedTime, temp); t.compose(temp); } } diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java index de31667c96cb..812ecd158b18 100644 --- a/core/java/android/view/animation/Transformation.java +++ b/core/java/android/view/animation/Transformation.java @@ -78,6 +78,7 @@ public class Transformation { mHasClipRect = false; mAlpha = 1.0f; mTransformationType = TYPE_BOTH; + mInsets = Insets.NONE; } /** diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index a07141b260ee..b7ee0b8a238a 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -520,6 +520,13 @@ public class WebChromeClient { * To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and * return {@code true}. * + * <p class="note"><b>Note:</b> WebView does not enforce any restrictions on + * the chosen file(s). WebView can access all files that your app can access. + * In case the file(s) are chosen through an untrusted source such as a third-party + * app, it is your own app's responsibility to check what the returned Uris + * refer to before calling the <code>filePathCallback</code>. See + * {@link #createIntent} and {@link #parseResult} for more details.</p> + * * @param webView The WebView instance that is initiating the request. * @param filePathCallback Invoke this callback to supply the list of paths to files to upload, * or {@code null} to cancel. Must only be called if the @@ -556,6 +563,15 @@ public class WebChromeClient { * Parse the result returned by the file picker activity. This method should be used with * {@link #createIntent}. Refer to {@link #createIntent} for how to use it. * + * <p class="note"><b>Note:</b> The intent returned by the file picker activity + * should be treated as untrusted. A third-party app handling the implicit + * intent created by {@link #createIntent} might return Uris that the third-party + * app itself does not have access to, such as your own app's sensitive data files. + * WebView does not enforce any restrictions on the returned Uris. It is the + * app's responsibility to ensure that the untrusted source (such as a third-party + * app) has access the Uris it has returned and that the Uris are not pointing + * to any sensitive data files.</p> + * * @param resultCode the integer result code returned by the file picker activity. * @param data the intent returned by the file picker activity. * @return the Uris of selected file(s) or {@code null} if the resultCode indicates @@ -618,6 +634,12 @@ public class WebChromeClient { * WebChromeClient#onShowFileChooser}</li> * </ol> * + * <p class="note"><b>Note:</b> The created intent may be handled by + * third-party applications on device. The received result must be treated + * as untrusted as it can contain Uris pointing to your own app's sensitive + * data files. Your app should check the resultant Uris in {@link #parseResult} + * before calling the <code>filePathCallback</code>.</p> + * * @return an Intent that supports basic file chooser sources. */ public abstract Intent createIntent(); diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index e8b4f0bec6a8..760c9166959c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -71,3 +71,10 @@ flag { description: "Enables quick switch for desktop mode" bug: "338066529" } + +flag { + name: "enable_additional_windows_above_status_bar" + namespace: "lse_desktop_experience" + description: "Allows for additional windows tied to WindowDecoration to be layered between status bar and notification shade." + bug: "316186265" +} diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index 33af48636f02..69cac6f3dfa5 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -49,3 +49,10 @@ flag { description: "Prevent BAL based on it is bound by foreground Uid but the app switch is stopped." bug: "283801068" } + +flag { + name: "bal_improved_metrics" + namespace: "responsible_apis" + description: "Improved metrics." + bug: "339245692" +} diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 12d62ccf97b6..062fab38656d 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -116,7 +116,7 @@ using android::base::GetBoolProperty; using android::zygote::ZygoteFailure; -using Action = android_mallopt_gwp_asan_options_t::Action; +using Mode = android_mallopt_gwp_asan_options_t::Mode; // This type is duplicated in fd_utils.h typedef const std::function<void(std::string)>& fail_fn_t; @@ -2101,21 +2101,21 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { default: case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT: - gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) - ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING - : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; + gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) + ? Mode::APP_MANIFEST_DEFAULT + : Mode::APP_MANIFEST_NEVER; android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); break; case RuntimeFlags::GWP_ASAN_LEVEL_NEVER: - gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; + gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER; android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); break; case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS: - gwp_asan_options.desire = Action::TURN_ON_FOR_APP; + gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS; android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); break; case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: - gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING; + gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT; android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); break; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index bfbfb3aa3d56..70d923b8a9bb 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8771,6 +8771,7 @@ <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService" android:permission="android.permission.BIND_INCALL_SERVICE" + android:enabled="@bool/config_enableContextSyncInCall" android:exported="true"> <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS" android:value="true" /> diff --git a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml index c0fe536cf85b..7c45c20a758b 100644 --- a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 11 13 L 2 22 L 11 22 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 1 bar. move to higher ground. --> + <path + android:name="ic_signal_cellular_1_4_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H11 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml index 816da22cf7dc..02b646d310e5 100644 --- a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M8.72,15.28,2,22H8.72V15.28Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 1 bar. might have to call you back. --> + <path + android:name="ic_signal_cellular_1_5_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H12 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml index 69a966ba4fc9..514d1690abcf 100644 --- a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 13 11 L 2 22 L 13 22 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 2 bars. 2 out of 4 ain't bad. --> + <path + android:name="ic_signal_cellular_2_4_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H14 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml index 02c7a430cece..a97f771a6632 100644 --- a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml @@ -23,7 +23,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 11.45 12.55 L 2 22 L 11.45 22 L 11.45 12.55 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 2 bars. hanging in there. --> + <path + android:name="ic_signal_cellular_2_5_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H14 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml index 46ce47cdfcda..1bacf4ad678f 100644 --- a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 2 22 L 16 22 L 16 21 L 16 20 L 16 11 L 16 10 L 16 8 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 3 bars. quite nice. --> + <path + android:name="ic_signal_cellular_3_4_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H17 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml index 37435e6b75e2..2789d3e9305c 100644 --- a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 14.96 9.04 L 2 22 L 14.96 22 L 14.96 9.04 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 3 bars. not great, not terrible. --> + <path + android:name="ic_signal_cellular_3_5_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H16 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml index 6dc3646d89e3..8286dbb5576f 100644 --- a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml +++ b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml @@ -22,7 +22,11 @@ <path android:fillColor="@android:color/white" android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" /> - <path - android:fillColor="@android:color/white" - android:pathData="M 18.48 5.52 L 2 22 L 18.48 22 L 18.48 5.52 Z" /> + <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z"> + <!-- 4 bars. extremely respectable. --> + <path + android:name="ic_signal_cellular_4_5_bar" + android:fillColor="@android:color/white" + android:pathData="M6,0 H18 V20 H6 z" /> + </clip-path> </vector>
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 877d11e0a09a..cefc648e45a5 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6093,6 +6093,18 @@ <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. --> <bool name="config_letterboxIsEducationEnabled">false</bool> + <!-- The width in dp to use to detect vertical thin letterboxing. + If W is the available width and w is the letterbox width, an app + is thin letterboxed if the value here is < (W - w) / 2 + If the value is < 0 the thin letterboxing policy is disabled --> + <dimen name="config_letterboxThinLetterboxWidthDp">-1dp</dimen> + + <!-- The height in dp to use to detect horizontal thin letterboxing + If H is the available height and h is the letterbox height, an app + is thin letterboxed if the value here is < (H - h) / 2 + If the value is < 0 the thin letterboxing policy is disabled --> + <dimen name="config_letterboxThinLetterboxHeightDp">-1dp</dimen> + <!-- Default min aspect ratio for unresizable apps which are eligible for size compat mode. Values <= 1.0 will be ignored. Activity min/max aspect ratio restrictions will still be espected so this override can control the maximum screen area that can be occupied by @@ -7039,6 +7051,9 @@ event gets ignored. --> <integer name="config_defaultMinEmergencyGestureTapDurationMillis">200</integer> + <!-- Control whether to enable CallMetadataSyncInCallService. --> + <bool name="config_enableContextSyncInCall">false</bool> + <!-- Whether the system uses auto-suspend mode. --> <bool name="config_useAutoSuspend">true</bool> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 1fca4f859294..59e4161b2e0b 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6484,4 +6484,23 @@ ul.</string> <string name="satellite_notification_how_it_works">How it works</string> <!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. --> <string name="unarchival_session_app_label">Pending...</string> + + <!-- Fingerprint dangling notification title --> + <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string> + <!-- Fingerprint dangling notification content for only 1 fingerprint deleted --> + <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted to improve performance</string> + <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted --> + <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted to improve performance</string> + <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left--> + <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string> + <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left --> + <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string> + <!-- Face dangling notification title --> + <string name="face_dangling_notification_title">Set up Face Unlock again</string> + <!-- Face dangling notification content --> + <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string> + <!-- Biometric dangling notification "set up" action button --> + <string name="biometric_dangling_notification_action_set_up">Set up</string> + <!-- Biometric dangling notification "Not now" action button --> + <string name="biometric_dangling_notification_action_not_now">Not now</string> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 54dbc48c99fb..d058fb188d16 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4717,6 +4717,8 @@ <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" /> <java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" /> <java-symbol type="bool" name="config_letterboxIsEducationEnabled" /> + <java-symbol type="dimen" name="config_letterboxThinLetterboxWidthDp" /> + <java-symbol type="dimen" name="config_letterboxThinLetterboxHeightDp" /> <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" /> <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" /> <java-symbol type="bool" name="config_letterboxIsDisplayAspectRatioForFixedOrientationLetterboxEnabled" /> @@ -5430,4 +5432,15 @@ <!-- For PowerManagerService to determine whether to use auto-suspend mode --> <java-symbol type="bool" name="config_useAutoSuspend" /> + + <!-- Biometric dangling notification strings --> + <java-symbol type="string" name="fingerprint_dangling_notification_title" /> + <java-symbol type="string" name="fingerprint_dangling_notification_msg_1" /> + <java-symbol type="string" name="fingerprint_dangling_notification_msg_2" /> + <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_1" /> + <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_2" /> + <java-symbol type="string" name="face_dangling_notification_title" /> + <java-symbol type="string" name="face_dangling_notification_msg" /> + <java-symbol type="string" name="biometric_dangling_notification_action_set_up" /> + <java-symbol type="string" name="biometric_dangling_notification_action_not_now" /> </resources> diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index 0b1b40c8ba8b..bc0ae9f31904 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -623,6 +623,28 @@ public class ViewFrameRateTest { assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory()); } + @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY + }) + public void idleDetected() throws Throwable { + waitForFrameRateCategoryToSettle(); + mActivityRule.runOnUiThread(() -> { + mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH); + mMovingView.setFrameContentVelocity(Float.MAX_VALUE); + mMovingView.invalidate(); + runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH, + mViewRoot.getLastPreferredFrameRateCategory())); + }); + waitForAfterDraw(); + + // Wait for idle timeout + Thread.sleep(500); + assertEquals(0f, mViewRoot.getLastPreferredFrameRate()); + assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE, + mViewRoot.getLastPreferredFrameRateCategory()); + } + private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS new file mode 100644 index 000000000000..08c70314973e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS @@ -0,0 +1,6 @@ +# WM shell sub-module bubble owner +madym@google.com +atsjenk@google.com +liranb@google.com +sukeshram@google.com +mpodolian@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt index 1e30d8feb132..ea86c79ae736 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt @@ -21,6 +21,7 @@ import android.app.WindowConfiguration import android.content.ComponentName import android.content.Context import android.os.RemoteException +import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.util.Pair @@ -137,5 +138,6 @@ object PipUtils { @JvmStatic val isPip2ExperimentEnabled: Boolean - get() = Flags.enablePip2Implementation() + get() = Flags.enablePip2Implementation() || SystemProperties.getBoolean( + "wm_shell.pip2", false) }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 17121c8de428..e729c7dd802b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -891,13 +891,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopTasksController> providesDesktopTasksController( + static Optional<DesktopTasksController> providesDesktopTasksController(Context context, @DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. return desktopTasksController.flatMap((lazy)-> { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } return Optional.empty(); @@ -910,13 +910,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository( + static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context, @DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) { // Use optional-of-lazy for the dependency that this provider relies on. // Lazy ensures that this provider will not be the cause the dependency is created // when it will not be returned due to the condition below. return desktopModeTaskRepository.flatMap((lazy)-> { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(lazy.get()); } return Optional.empty(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index b574b8159307..a1910c5eb3a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -221,7 +221,7 @@ public abstract class WMShellModule { Transitions transitions, Optional<DesktopTasksController> desktopTasksController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, mainExecutor, @@ -278,8 +278,8 @@ public abstract class WMShellModule { ShellInit init = FreeformComponents.isFreeformEnabled(context) ? shellInit : null; - return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository, - windowDecorViewModel); + return new FreeformTaskListener(context, init, shellTaskOrganizer, + desktopModeTaskRepository, windowDecorViewModel); } @WMSingleton @@ -535,10 +535,12 @@ public abstract class WMShellModule { @WMSingleton @Provides static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter( + Context context, Transitions transitions, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, ShellTaskOrganizer shellTaskOrganizer) { - if (!DesktopModeStatus.isEnabled() || !Flags.enableDesktopWindowingTaskLimit()) { + if (!DesktopModeStatus.canEnterDesktopMode(context) + || !Flags.enableDesktopWindowingTaskLimit()) { return Optional.empty(); } return Optional.of( @@ -592,23 +594,26 @@ public abstract class WMShellModule { @WMSingleton @Provides static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( + Context context, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, Transitions transitions, ShellInit shellInit ) { return desktopModeTaskRepository.flatMap(repository -> - Optional.of(new DesktopTasksTransitionObserver(repository, transitions, shellInit)) + Optional.of(new DesktopTasksTransitionObserver( + context, repository, transitions, shellInit)) ); } @WMSingleton @Provides static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( + Context context, ShellInit shellInit, Transitions transitions, DesktopModeEventLogger desktopModeEventLogger) { return new DesktopModeLoggerTransitionObserver( - shellInit, transitions, desktopModeEventLogger); + context, shellInit, transitions, desktopModeEventLogger); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index a10c7c093c60..9038aaad9178 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -20,6 +20,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityTaskManager.INVALID_TASK_ID import android.app.TaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context import android.os.IBinder import android.util.SparseArray import android.view.SurfaceControl @@ -49,6 +50,7 @@ import com.android.wm.shell.util.KtProtoLog * and other transitions that originate both within and outside shell. */ class DesktopModeLoggerTransitionObserver( + context: Context, shellInit: ShellInit, private val transitions: Transitions, private val desktopModeEventLogger: DesktopModeEventLogger @@ -57,7 +59,8 @@ class DesktopModeLoggerTransitionObserver( private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) } init { - if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + if (Transitions.ENABLE_SHELL_TRANSITIONS && + DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(this::onInit, this) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index fcddcad3a949..9bf9fa749373 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -93,8 +93,10 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT); /** - * Return {@code true} if desktop windowing is enabled + * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers + * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing. */ + @VisibleForTesting public static boolean isEnabled() { return Flags.enableDesktopWindowingMode(); } @@ -155,9 +157,9 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop mode can be entered on the current device. + * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context); + return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index ecfb134e45a0..f7bfb86a5158 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -167,7 +167,7 @@ class DesktopTasksController( init { desktopMode = DesktopModeImpl() - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback({ onInit() }, this) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 20df26428649..451e09c3cd9c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import android.content.Context import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager @@ -33,13 +34,15 @@ import com.android.wm.shell.util.KtProtoLog * and other transitions that originate both within and outside shell. */ class DesktopTasksTransitionObserver( + context: Context, private val desktopModeTaskRepository: DesktopModeTaskRepository, private val transitions: Transitions, shellInit: ShellInit ) : Transitions.TransitionObserver { init { - if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + if (Transitions.ENABLE_SHELL_TRANSITIONS && + DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::onInit, this) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 6fea2036dbd1..a414a55eb633 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM; import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; import android.util.SparseArray; import android.view.SurfaceControl; @@ -44,6 +45,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ShellTaskOrganizer.FocusListener { private static final String TAG = "FreeformTaskListener"; + private final Context mContext; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository; private final WindowDecorViewModel mWindowDecorationViewModel; @@ -56,10 +58,12 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, } public FreeformTaskListener( + Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Optional<DesktopModeTaskRepository> desktopModeTaskRepository, WindowDecorViewModel windowDecorationViewModel) { + mContext = context; mShellTaskOrganizer = shellTaskOrganizer; mWindowDecorationViewModel = windowDecorationViewModel; mDesktopModeTaskRepository = desktopModeTaskRepository; @@ -70,7 +74,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private void onInit() { mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mShellTaskOrganizer.addFocusListener(this); } } @@ -92,7 +96,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, t.apply(); } - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); @@ -114,7 +118,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mTasks.remove(taskInfo.taskId); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.taskId); repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); @@ -125,7 +129,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false); }); } - + mWindowDecorationViewModel.onTaskVanished(taskInfo); if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mWindowDecorationViewModel.destroyWindowDecoration(taskInfo); } @@ -139,7 +143,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mWindowDecorationViewModel.onTaskInfoChanged(taskInfo); state.mTaskInfo = taskInfo; - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopModeTaskRepository.ifPresent(repository -> { if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { @@ -161,7 +165,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Focus Changed: #%d focused=%b", taskInfo.taskId, taskInfo.isFocused); - if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) { + if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index 998728d65e6a..2626e7380163 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -161,7 +161,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", taskInfo.taskId); mTasks.remove(taskInfo.taskId); - + mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo)); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; if (mWindowDecorViewModelOptional.isPresent()) { mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index c5f23a8ed034..a8611d966a29 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -327,7 +327,8 @@ public class RecentTasksController implements TaskStackListenerCallback, private boolean shouldEnableRunningTasksForDesktopMode() { return mPcFeatureEnabled - || (DesktopModeStatus.isEnabled() && enableDesktopWindowingTaskbarRunningApps()); + || (DesktopModeStatus.canEnterDesktopMode(mContext) + && enableDesktopWindowingTaskbarRunningApps()); } @VisibleForTesting @@ -371,7 +372,8 @@ public class RecentTasksController implements TaskStackListenerCallback, continue; } - if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent() + if (DesktopModeStatus.canEnterDesktopMode(mContext) + && mDesktopModeTaskRepository.isPresent() && mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) { if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) { // Minimized freeform tasks should not be shown at all. 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 f41bca36bb70..1e305c5dbbcf 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 @@ -260,6 +260,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId); final int taskId = taskInfo.taskId; + mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo)); if (mRootTaskInfo.taskId == taskId) { mCallbacks.onRootTaskVanished(); mRootTaskInfo = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 87dc3915082f..e85cb6400000 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -119,6 +119,21 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + // A task vanishing doesn't necessarily mean the task was closed, it could also mean its + // windowing mode changed. We're only interested in closing tasks so checking whether + // its info still exists in the task organizer is one way to disambiguate. + final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null; + if (closed) { + // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition + // changes happen, but there are certain cases in which closing tasks aren't included + // in transitions, such as when a non-visible task is closed. See b/296921167. + // Destroy the decoration here in case the lack of transition missed it. + destroyWindowDecoration(taskInfo); + } + } + + @Override public void onTaskChanging( RunningTaskInfo taskInfo, SurfaceControl taskSurface, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 01175f598089..f3ef7c17fac3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -35,6 +35,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.annotation.NonNull; @@ -72,6 +73,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; @@ -276,7 +278,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { if (visible && stage != STAGE_TYPE_UNDEFINED) { DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); - if (decor != null && DesktopModeStatus.isEnabled()) { + if (decor != null && DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopTasksController.moveToSplit(decor.mTaskInfo); } } @@ -309,6 +311,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + // A task vanishing doesn't necessarily mean the task was closed, it could also mean its + // windowing mode changed. We're only interested in closing tasks so checking whether + // its info still exists in the task organizer is one way to disambiguate. + final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null; + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Task Vanished: #%d closed=%b", taskInfo.taskId, closed); + if (closed) { + // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition + // changes happen, but there are certain cases in which closing tasks aren't included + // in transitions, such as when a non-visible task is closed. See b/296921167. + // Destroy the decoration here in case the lack of transition missed it. + destroyWindowDecoration(taskInfo); + } + } + + @Override public void onTaskChanging( RunningTaskInfo taskInfo, SurfaceControl taskSurface, @@ -594,7 +612,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { public boolean handleMotionEvent(@Nullable View v, MotionEvent e) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final RunningTaskInfo taskInfo = decoration.mTaskInfo; - if (DesktopModeStatus.isEnabled() + if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { return false; } @@ -771,7 +789,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) { final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev); - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { if (!mInImmersiveMode && (relevantDecor == null || relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM || mTransitionDragActive)) { @@ -780,7 +798,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } handleEventOutsideCaption(ev, relevantDecor); // Prevent status bar from reacting to a caption drag. - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { if (mTransitionDragActive) { inputMonitor.pilferPointers(); } @@ -838,7 +856,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragToDesktopAnimationStartBounds.set( relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; - if (DesktopModeStatus.isEnabled()) { + if (DesktopModeStatus.canEnterDesktopMode(mContext)) { // In proto2 any full screen or multi-window task can be dragged to // freeform. final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); @@ -1013,12 +1031,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && isSingleTopActivityTranslucent(taskInfo)) { return false; } - return DesktopModeStatus.isEnabled() + return DesktopModeStatus.canEnterDesktopMode(mContext) && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD - && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop() - && DesktopModeStatus.canEnterDesktopMode(mContext); + && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); } private void createWindowDecoration( @@ -1087,7 +1104,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private void dump(PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + "DesktopModeWindowDecorViewModel"); - pw.println(innerPrefix + "DesktopModeStatus=" + DesktopModeStatus.isEnabled()); + pw.println(innerPrefix + "DesktopModeStatus=" + + DesktopModeStatus.canEnterDesktopMode(mContext)); pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive); pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay); pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 8c6bc73d10bf..f7516dabe1ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -638,7 +638,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setOnClickListener(mOnCaptionButtonClickListener) .setOnTouchListener(mOnCaptionTouchListener) .setLayoutId(mRelayoutParams.mLayoutResId) - .setWindowingButtonsVisible(DesktopModeStatus.isEnabled()) + .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext)) .setCaptionHeight(mResult.mCaptionHeight) .build(); mWindowDecorViewHolder.onHandleMenuOpened(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index 01a6012ea314..1563259f4a1a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -67,6 +67,14 @@ public interface WindowDecorViewModel { void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo); /** + * Notifies a task has vanished, which can mean that the task changed windowing mode or was + * removed. + * + * @param taskInfo the task info of the task + */ + void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo); + + /** * Notifies a transition is about to start about the given task to give the window decoration a * chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates * a window decoration if one does not exist but is required. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index 65117f7e9eea..60a7dcda5351 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.content.Context import android.os.IBinder import android.testing.AndroidTestingRunner import android.view.SurfaceControl @@ -35,6 +36,7 @@ import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.WindowContainerToken import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason @@ -58,6 +60,11 @@ import org.mockito.kotlin.never import org.mockito.kotlin.same import org.mockito.kotlin.times +/** + * Test class for {@link DesktopModeLoggerTransitionObserver} + * + * Usage: atest WMShellUnitTests:DesktopModeLoggerTransitionObserverTest + */ @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopModeLoggerTransitionObserverTest { @@ -74,6 +81,8 @@ class DesktopModeLoggerTransitionObserverTest { private lateinit var mockShellInit: ShellInit @Mock private lateinit var transitions: Transitions + @Mock + private lateinit var context: Context private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver private lateinit var shellInit: ShellInit @@ -81,12 +90,12 @@ class DesktopModeLoggerTransitionObserverTest { @Before fun setup() { - Mockito.`when`(DesktopModeStatus.isEnabled()).thenReturn(true) + doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } shellInit = Mockito.spy(ShellInit(testExecutor)) desktopModeEventLogger = mock(DesktopModeEventLogger::class.java) transitionObserver = DesktopModeLoggerTransitionObserver( - mockShellInit, transitions, desktopModeEventLogger) + context, mockShellInit, transitions, desktopModeEventLogger) if (Transitions.ENABLE_SHELL_TRANSITIONS) { val initRunnableCaptor = ArgumentCaptor.forClass( Runnable::class.java) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index ad4b720facd7..df8a2223dadc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -159,6 +159,7 @@ class DesktopTasksControllerTest : ShellTestCase() { mockitoSession = mockitoSession().strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java).startMocking() whenever(DesktopModeStatus.isEnabled()).thenReturn(true) + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } shellInit = Mockito.spy(ShellInit(testExecutor)) desktopModeTaskRepository = DesktopModeTaskRepository() @@ -1347,7 +1348,6 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFullscreenTask(displayId) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) runningTasks.add(task) @@ -1356,7 +1356,6 @@ class DesktopTasksControllerTest : ShellTestCase() { private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createSplitScreenTask(displayId) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 38ea03471b07..539d5b86453f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -27,6 +27,7 @@ import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase @@ -41,6 +42,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.any import org.mockito.Mockito.`when` import org.mockito.quality.Strictness @@ -69,7 +71,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { fun setUp() { mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java).startMocking() - `when`(DesktopModeStatus.isEnabled()).thenReturn(true) + doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } desktopTaskRepo = DesktopModeTaskRepository() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 71eea4bb59b1..665077be3af7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -19,11 +19,12 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager; @@ -72,8 +73,10 @@ public final class FreeformTaskListenerTests extends ShellTestCase { public void setup() { mMockitoSession = mockitoSession().initMocks(this) .strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(true); + doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mFreeformTaskListener = new FreeformTaskListener( + mContext, mShellInit, mTaskOrganizer, Optional.of(mDesktopModeTaskRepository), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 5cf9be4e7aca..240324ba4420 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -59,6 +59,7 @@ import android.view.SurfaceControl; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; @@ -75,11 +76,13 @@ import com.android.wm.shell.sysui.ShellSharedConstants; import com.android.wm.shell.util.GroupedRecentTaskInfo; import com.android.wm.shell.util.SplitBounds; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.quality.Strictness; import java.util.ArrayList; import java.util.Arrays; @@ -88,7 +91,9 @@ import java.util.Optional; import java.util.function.Consumer; /** - * Tests for {@link RecentTasksController}. + * Tests for {@link RecentTasksController} + * + * Usage: atest WMShellUnitTests:RecentTasksControllerTest */ @RunWith(AndroidJUnit4.class) @SmallTest @@ -118,9 +123,15 @@ public class RecentTasksControllerTest extends ShellTestCase { private ShellInit mShellInit; private ShellController mShellController; private TestShellExecutor mMainExecutor; + private static StaticMockitoSession sMockitoSession; @Before public void setUp() { + sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus.class).startMocking(); + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mMainExecutor = new TestShellExecutor(); when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); @@ -136,6 +147,11 @@ public class RecentTasksControllerTest extends ShellTestCase { mShellInit.init(); } + @After + public void tearDown() { + sMockitoSession.finishMocking(); + } + @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class)); @@ -275,10 +291,6 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() { - StaticMockitoSession mockitoSession = mockitoSession().mockStatic( - DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(true); - ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); @@ -309,16 +321,10 @@ public class RecentTasksControllerTest extends ShellTestCase { // Check single entries assertEquals(t2, singleGroup1.getTaskInfo1()); assertEquals(t4, singleGroup2.getTaskInfo1()); - - mockitoSession.finishMocking(); } @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() { - StaticMockitoSession mockitoSession = mockitoSession().mockStatic( - DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(true); - ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); @@ -357,15 +363,12 @@ public class RecentTasksControllerTest extends ShellTestCase { // Check single entry assertEquals(t4, singleGroup.getTaskInfo1()); - - mockitoSession.finishMocking(); } @Test public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() { - StaticMockitoSession mockitoSession = mockitoSession().mockStatic( - DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(false); + ExtendedMockito.doReturn(false) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); @@ -390,16 +393,10 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(t2, recentTasks.get(1).getTaskInfo1()); assertEquals(t3, recentTasks.get(2).getTaskInfo1()); assertEquals(t4, recentTasks.get(3).getTaskInfo1()); - - mockitoSession.finishMocking(); } @Test public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() { - StaticMockitoSession mockitoSession = mockitoSession().mockStatic( - DesktopModeStatus.class).startMocking(); - when(DesktopModeStatus.isEnabled()).thenReturn(true); - ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); @@ -435,8 +432,6 @@ public class RecentTasksControllerTest extends ShellTestCase { // Check single entries assertEquals(t2, singleGroup1.getTaskInfo1()); assertEquals(t4, singleGroup2.getTaskInfo1()); - - mockitoSession.finishMocking(); } @Test diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index 933a33eb8b0e..f97992f7c9d1 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -24,7 +24,6 @@ #include <SkColor.h> #include <android-base/stringprintf.h> #include <android-base/thread_annotations.h> -#include <com_android_input_flags.h> #include <ftl/enum.h> #include <mutex> @@ -35,14 +34,10 @@ #define INDENT2 " " #define INDENT3 " " -namespace input_flags = com::android::input::flags; - namespace android { namespace { -static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer(); - const ui::Transform kIdentityTransform; } // namespace @@ -68,27 +63,24 @@ void PointerController::DisplayInfoListener::onPointerControllerDestroyed() { std::shared_ptr<PointerController> PointerController::create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, ControllerType type) { + SpriteController& spriteController, ControllerType type) { // using 'new' to access non-public constructor std::shared_ptr<PointerController> controller; switch (type) { case ControllerType::MOUSE: controller = std::shared_ptr<PointerController>( - new MousePointerController(policy, looper, spriteController, enabled)); + new MousePointerController(policy, looper, spriteController)); break; case ControllerType::TOUCH: controller = std::shared_ptr<PointerController>( - new TouchPointerController(policy, looper, spriteController, enabled)); + new TouchPointerController(policy, looper, spriteController)); break; case ControllerType::STYLUS: controller = std::shared_ptr<PointerController>( - new StylusPointerController(policy, looper, spriteController, enabled)); + new StylusPointerController(policy, looper, spriteController)); break; - case ControllerType::LEGACY: default: - controller = std::shared_ptr<PointerController>( - new PointerController(policy, looper, spriteController, enabled)); - break; + LOG_ALWAYS_FATAL("Invalid ControllerType: %d", static_cast<int>(type)); } /* @@ -108,10 +100,9 @@ std::shared_ptr<PointerController> PointerController::create( } PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled) + const sp<Looper>& looper, SpriteController& spriteController) : PointerController( - policy, looper, spriteController, enabled, + policy, looper, spriteController, [](const sp<android::gui::WindowInfosListener>& listener) { auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{}, std::vector<android::gui::DisplayInfo>{}); @@ -125,11 +116,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, - bool enabled, const WindowListenerRegisterConsumer& registerListener, WindowListenerUnregisterConsumer unregisterListener) - : mEnabled(enabled), - mContext(policy, looper, spriteController, *this), + : mContext(policy, looper, spriteController, *this), mCursorController(mContext), mDisplayInfoListener(sp<DisplayInfoListener>::make(this)), mUnregisterWindowInfosListener(std::move(unregisterListener)) { @@ -142,7 +131,6 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::~PointerController() { mDisplayInfoListener->onPointerControllerDestroyed(); mUnregisterWindowInfosListener(mDisplayInfoListener); - mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0}); } std::mutex& PointerController::getLock() const { @@ -150,14 +138,10 @@ std::mutex& PointerController::getLock() const { } std::optional<FloatRect> PointerController::getBounds() const { - if (!mEnabled) return {}; - return mCursorController.getBounds(); } void PointerController::move(float deltaX, float deltaY) { - if (!mEnabled) return; - const int32_t displayId = mCursorController.getDisplayId(); vec2 transformed; { @@ -169,8 +153,6 @@ void PointerController::move(float deltaX, float deltaY) { } void PointerController::setPosition(float x, float y) { - if (!mEnabled) return; - const int32_t displayId = mCursorController.getDisplayId(); vec2 transformed; { @@ -182,10 +164,6 @@ void PointerController::setPosition(float x, float y) { } FloatPoint PointerController::getPosition() const { - if (!mEnabled) { - return FloatPoint{0, 0}; - } - const int32_t displayId = mCursorController.getDisplayId(); const auto p = mCursorController.getPosition(); { @@ -196,28 +174,20 @@ FloatPoint PointerController::getPosition() const { } int32_t PointerController::getDisplayId() const { - if (!mEnabled) return ADISPLAY_ID_NONE; - return mCursorController.getDisplayId(); } void PointerController::fade(Transition transition) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.fade(transition); } void PointerController::unfade(Transition transition) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.unfade(transition); } void PointerController::setPresentation(Presentation presentation) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); if (mLocked.presentation == presentation) { @@ -226,33 +196,13 @@ void PointerController::setPresentation(Presentation presentation) { mLocked.presentation = presentation; - if (ENABLE_POINTER_CHOREOGRAPHER) { - // When pointer choreographer is enabled, the presentation mode is only set once when the - // PointerController is constructed, before the display viewport is provided. - // TODO(b/293587049): Clean up the PointerController interface after pointer choreographer - // is permanently enabled. The presentation can be set in the constructor. - mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); - return; - } - - if (!mCursorController.isViewportValid()) { - return; - } - - if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) { - // For now, we support stylus hover using the mouse cursor implementation. - // TODO: Add proper support for stylus hover icons. - mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); - - mCursorController.getAdditionalMouseResources(); - clearSpotsLocked(); - } + // The presentation mode is only set once when the PointerController is constructed, + // before the display viewport is provided. + mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER); } void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex, BitSet32 spotIdBits, int32_t displayId) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); std::array<PointerCoords, MAX_POINTERS> outSpotCoords{}; const ui::Transform& transform = getTransformForDisplayLocked(displayId); @@ -279,8 +229,6 @@ void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t } void PointerController::clearSpots() { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); clearSpotsLocked(); } @@ -313,12 +261,6 @@ void PointerController::reloadPointerResources() { } void PointerController::setDisplayViewport(const DisplayViewport& viewport) { - struct PointerDisplayChangeArgs { - int32_t displayId; - FloatPoint cursorPosition; - }; - std::optional<PointerDisplayChangeArgs> pointerDisplayChanged; - { // acquire lock std::scoped_lock lock(getLock()); @@ -330,34 +272,21 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) { mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources); if (viewport.displayId != mLocked.pointerDisplayId) { mLocked.pointerDisplayId = viewport.displayId; - pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()}; } } // release lock - - if (pointerDisplayChanged) { - // Notify the policy without holding the pointer controller lock. - mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId, - pointerDisplayChanged->cursorPosition); - } } void PointerController::updatePointerIcon(PointerIconStyle iconId) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.updatePointerIcon(iconId); } void PointerController::setCustomPointerIcon(const SpriteIcon& icon) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); mCursorController.setCustomPointerIcon(icon); } void PointerController::setSkipScreenshot(int32_t displayId, bool skip) { - if (!mEnabled) return; - std::scoped_lock lock(getLock()); if (skip) { mLocked.displaysToSkipScreenshot.insert(displayId); @@ -406,10 +335,6 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display } std::string PointerController::dump() { - if (!mEnabled) { - return INDENT "PointerController: DISABLED due to ongoing PointerChoreographer refactor\n"; - } - std::string dump = INDENT "PointerController:\n"; std::scoped_lock lock(getLock()); dump += StringPrintf(INDENT2 "Presentation: %s\n", @@ -430,8 +355,8 @@ std::string PointerController::dump() { MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::POINTER); } @@ -443,8 +368,8 @@ MousePointerController::~MousePointerController() { TouchPointerController::TouchPointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::SPOT); } @@ -456,8 +381,8 @@ TouchPointerController::~TouchPointerController() { StylusPointerController::StylusPointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) - : PointerController(policy, looper, spriteController, enabled) { + SpriteController& spriteController) + : PointerController(policy, looper, spriteController) { PointerController::setPresentation(Presentation::STYLUS_HOVER); } diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index d76ca5d15a31..eaf34d527396 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -47,8 +47,7 @@ class PointerController : public PointerControllerInterface { public: static std::shared_ptr<PointerController> create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, - ControllerType type = ControllerType::LEGACY); + SpriteController& spriteController, ControllerType type); ~PointerController() override; @@ -87,12 +86,12 @@ protected: // Constructor used to test WindowInfosListener registration. PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled, + SpriteController& spriteController, const WindowListenerRegisterConsumer& registerListener, WindowListenerUnregisterConsumer unregisterListener); PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled); + SpriteController& spriteController); private: friend PointerControllerContext::LooperCallback; @@ -104,8 +103,6 @@ private: // we use the DisplayInfoListener's lock in PointerController. std::mutex& getLock() const; - const bool mEnabled; - PointerControllerContext mContext; MouseCursorController mCursorController; @@ -144,8 +141,7 @@ class MousePointerController : public PointerController { public: /** A version of PointerController that controls one mouse pointer. */ MousePointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~MousePointerController() override; @@ -164,8 +160,7 @@ class TouchPointerController : public PointerController { public: /** A version of PointerController that controls touch spots. */ TouchPointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~TouchPointerController() override; @@ -210,8 +205,7 @@ class StylusPointerController : public PointerController { public: /** A version of PointerController that controls one stylus pointer. */ StylusPointerController(const sp<PointerControllerPolicyInterface>& policy, - const sp<Looper>& looper, SpriteController& spriteController, - bool enabled); + const sp<Looper>& looper, SpriteController& spriteController); ~StylusPointerController() override; diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h index 98c3988e7df4..e893c49d80e5 100644 --- a/libs/input/PointerControllerContext.h +++ b/libs/input/PointerControllerContext.h @@ -81,7 +81,6 @@ public: virtual PointerIconStyle getDefaultPointerIconId() = 0; virtual PointerIconStyle getDefaultStylusIconId() = 0; virtual PointerIconStyle getCustomPointerIconId() = 0; - virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0; }; /* diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h index 4e4ba6551aec..fdb15506fd0c 100644 --- a/libs/input/SpriteController.h +++ b/libs/input/SpriteController.h @@ -165,29 +165,23 @@ private: * on the sprites for a long time. * Note that the SpriteIcon holds a reference to a shared (and immutable) bitmap. */ struct SpriteState { - inline SpriteState() : - dirty(0), visible(false), - positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT), - surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) { - } - - uint32_t dirty; + uint32_t dirty{0}; SpriteIcon icon; - bool visible; - float positionX; - float positionY; - int32_t layer; - float alpha; + bool visible{false}; + float positionX{0}; + float positionY{0}; + int32_t layer{0}; + float alpha{1.0f}; SpriteTransformationMatrix transformationMatrix; - int32_t displayId; + int32_t displayId{ADISPLAY_ID_DEFAULT}; sp<SurfaceControl> surfaceControl; - int32_t surfaceWidth; - int32_t surfaceHeight; - bool surfaceDrawn; - bool surfaceVisible; - bool skipScreenshot; + int32_t surfaceWidth{0}; + int32_t surfaceHeight{0}; + bool surfaceDrawn{false}; + bool surfaceVisible{false}; + bool skipScreenshot{false}; inline bool wantSurfaceVisible() const { return visible && alpha > 0.0f && icon.isValid(); diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index fcf226c9b949..3bc0e24b6e2e 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -14,7 +14,6 @@ * limitations under the License. */ -#include <com_android_input_flags.h> #include <flag_macros.h> #include <gmock/gmock.h> #include <gtest/gtest.h> @@ -30,8 +29,6 @@ namespace android { -namespace input_flags = com::android::input::flags; - enum TestCursorType { CURSOR_TYPE_DEFAULT = 0, CURSOR_TYPE_HOVER, @@ -64,11 +61,9 @@ public: virtual PointerIconStyle getDefaultPointerIconId() override; virtual PointerIconStyle getDefaultStylusIconId() override; virtual PointerIconStyle getCustomPointerIconId() override; - virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; bool allResourcesAreLoaded(); bool noResourcesAreLoaded(); - std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; } private: void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType); @@ -76,7 +71,6 @@ private: bool pointerIconLoaded{false}; bool pointerResourcesLoaded{false}; bool additionalMouseResourcesLoaded{false}; - std::optional<int32_t /*displayId*/> latestPointerDisplayId; }; void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) { @@ -146,12 +140,6 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic icon->hotSpotY = hotSpot.second; } -void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId, - const FloatPoint& /*position*/ -) { - latestPointerDisplayId = displayId; -} - class TestPointerController : public PointerController { public: TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener, @@ -159,7 +147,6 @@ public: SpriteController& spriteController) : PointerController( policy, looper, spriteController, - /*enabled=*/true, [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) -> std::vector<gui::DisplayInfo> { // Register listener @@ -267,8 +254,7 @@ TEST_F(PointerControllerTest, useStylusTypeForStylusHover) { mPointerController->reloadPointerResources(); } -TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) { +TEST_F(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources) { // Setting the presentation mode before a display viewport is set will not load any resources. mPointerController->setPresentation(PointerController::Presentation::POINTER); ASSERT_TRUE(mPolicy->noResourcesAreLoaded()); @@ -278,26 +264,7 @@ TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoe ASSERT_TRUE(mPolicy->allResourcesAreLoaded()); } -TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIcon, - REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(input_flags, - enable_pointer_choreographer))) { - ensureDisplayViewportIsSet(); - mPointerController->setPresentation(PointerController::Presentation::POINTER); - mPointerController->unfade(PointerController::Transition::IMMEDIATE); - - int32_t type = CURSOR_TYPE_ADDITIONAL; - std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type); - EXPECT_CALL(*mPointerSprite, setVisible(true)); - EXPECT_CALL(*mPointerSprite, setAlpha(1.0f)); - EXPECT_CALL(*mPointerSprite, - setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)), - Field(&SpriteIcon::hotSpotX, hotspot.first), - Field(&SpriteIcon::hotSpotY, hotspot.second)))); - mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type)); -} - -TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIconWithChoreographer, - REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) { +TEST_F(PointerControllerTest, updatePointerIconWithChoreographer) { // When PointerChoreographer is enabled, the presentation mode is set before the viewport. mPointerController->setPresentation(PointerController::Presentation::POINTER); ensureDisplayViewportIsSet(); @@ -348,30 +315,6 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) { ensureDisplayViewportIsSet(); } -TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) { - EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId()) - << "A pointer display change does not occur when PointerController is created."; - - ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT); - - const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId(); - ASSERT_TRUE(lastReportedPointerDisplayId) - << "The policy is notified of a pointer display change when the viewport is first set."; - EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId) - << "Incorrect pointer display notified."; - - ensureDisplayViewportIsSet(42); - - EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId()) - << "The policy is notified when the pointer display changes."; - - // Release the PointerController. - mPointerController = nullptr; - - EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId()) - << "The pointer display changes to invalid when PointerController is destroyed."; -} - TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) { ensureDisplayViewportIsSet(); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index b2838c809854..7ddf11e41a41 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -50,6 +50,7 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.media.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -984,37 +985,7 @@ public final class MediaRouter2 { @SystemApi @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) { - mImpl.transfer( - controller.getRoutingSessionInfo(), - route, - Process.myUserHandle(), - mContext.getPackageName()); - } - - /** - * Transfers the media of a routing controller to the given route. - * - * <p>This will be no-op for non-system media routers. - * - * @param controller a routing controller controlling media routing. - * @param route the route you want to transfer the media to. - * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer - * request. - * @param transferInitiatorPackageName the package name of the app that initiated the transfer. - * This value is used with the user handle to populate {@link - * RoutingController#wasTransferInitiatedBySelf()}. - * @hide - */ - public void transfer( - @NonNull RoutingController controller, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { - mImpl.transfer( - controller.getRoutingSessionInfo(), - route, - transferInitiatorUserHandle, - transferInitiatorPackageName); + mImpl.transfer(controller.getRoutingSessionInfo(), route); } void requestCreateController( @@ -1913,13 +1884,7 @@ public final class MediaRouter2 { */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) public boolean wasTransferInitiatedBySelf() { - RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); - - UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); - String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); - - return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle) - && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName); + return mImpl.wasTransferredBySelf(getRoutingSessionInfo()); } /** @@ -2082,12 +2047,28 @@ public final class MediaRouter2 { Objects.requireNonNull(route, "route must not be null"); synchronized (mControllerLock) { if (isReleased()) { - Log.w(TAG, "transferToRoute: Called on released controller. Ignoring."); + Log.w( + TAG, + "tryTransferWithinProvider: Called on released controller. Ignoring."); return true; } - if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) { - Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route); + // If this call is trying to transfer to a selected system route, we let them + // through as a provider driven transfer in order to update the transfer reason and + // initiator data. + boolean isSystemRouteReselection = + Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() + && mSessionInfo.isSystemSession() + && route.isSystemRoute() + && mSessionInfo.getSelectedRoutes().contains(route.getId()); + if (!isSystemRouteReselection + && !mSessionInfo.getTransferableRoutes().contains(route.getId())) { + Log.i( + TAG, + "Transferring to a non-transferable route=" + + route + + " session= " + + mSessionInfo.getId()); return false; } } @@ -2498,11 +2479,7 @@ public final class MediaRouter2 { void stop(); - void transfer( - @NonNull RoutingSessionInfo sessionInfo, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName); + void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route); List<RoutingController> getControllers(); @@ -2523,6 +2500,11 @@ public final class MediaRouter2 { boolean shouldNotifyStop, RoutingController controller); + /** + * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app + * associated with this router. + */ + boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo); } /** @@ -2723,7 +2705,7 @@ public final class MediaRouter2 { List<RoutingSessionInfo> sessionInfos = getRoutingSessions(); RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1); - transfer(targetSession, route, mClientUser, mContext.getPackageName()); + transfer(targetSession, route); } @Override @@ -2746,24 +2728,15 @@ public final class MediaRouter2 { * * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer. * @param route The {@link MediaRoute2Info route} to transfer to. - * @param transferInitiatorUserHandle The user handle of the app that initiated the - * transfer. - * @param transferInitiatorPackageName The package name if of the app that initiated the - * transfer. * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String) * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info) */ @Override @SuppressWarnings("AndroidFrameworkRequiresPermission") public void transfer( - @NonNull RoutingSessionInfo sessionInfo, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { + @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) { Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); Objects.requireNonNull(route, "route must not be null"); - Objects.requireNonNull(transferInitiatorUserHandle); - Objects.requireNonNull(transferInitiatorPackageName); Log.v( TAG, @@ -2780,15 +2753,19 @@ public final class MediaRouter2 { return; } - if (sessionInfo.getTransferableRoutes().contains(route.getId())) { - transferToRoute( - sessionInfo, - route, - transferInitiatorUserHandle, - transferInitiatorPackageName); + // If this call is trying to transfer to a selected system route, we let them + // through as a provider driven transfer in order to update the transfer reason and + // initiator data. + boolean isSystemRouteReselection = + Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() + && sessionInfo.isSystemSession() + && route.isSystemRoute() + && sessionInfo.getSelectedRoutes().contains(route.getId()); + if (sessionInfo.getTransferableRoutes().contains(route.getId()) + || isSystemRouteReselection) { + transferToRoute(sessionInfo, route, mClientUser, mClientPackageName); } else { - requestCreateSession(sessionInfo, route, transferInitiatorUserHandle, - transferInitiatorPackageName); + requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName); } } @@ -3043,6 +3020,14 @@ public final class MediaRouter2 { releaseSession(controller.getRoutingSessionInfo()); } + @Override + public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) { + UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); + String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); + return Objects.equals(mClientUser, transferInitiatorUserHandle) + && Objects.equals(mClientPackageName, transferInitiatorPackageName); + } + /** * Retrieves the system session info for the given package. * @@ -3619,10 +3604,7 @@ public final class MediaRouter2 { */ @Override public void transfer( - @NonNull RoutingSessionInfo sessionInfo, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { + @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) { // Do nothing. } @@ -3741,6 +3723,14 @@ public final class MediaRouter2 { } } + @Override + public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) { + UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); + String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName(); + return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle) + && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName); + } + @GuardedBy("mLock") private void registerRouterStubIfNeededLocked() throws RemoteException { if (mStub == null) { diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index de9eada18104..2fe2ce39813b 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -1212,16 +1212,16 @@ public final class CardEmulation { * * @param service The ComponentName of the service * @param status true to enable, false to disable + * @param userId the user handle of the user whose information is being requested. * @return set service for the category and true if service is already set return false. * * @hide */ - public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) { + public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status, + int userId) { if (service == null) { throw new NullPointerException("activity or service or category is null"); } - int userId = mContext.getUser().getIdentifier(); - try { return sService.setServiceEnabledForCategoryOther(userId, service, status); } catch (RemoteException e) { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 65c570840368..fce7a00f9744 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -382,6 +382,7 @@ android_library { "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", + "device_policy_aconfig_flags_lib", ], libs: [ "keepanno-annotations", @@ -622,6 +623,7 @@ android_app { "//frameworks/libs/systemui:compilelib", "SystemUI-tests-base", "androidx.compose.runtime_runtime", + "SystemUI-core", ], libs: [ "keepanno-annotations", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 59e2b9175711..b9e70ef14007 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -1110,7 +1110,7 @@ android:resource="@xml/home_controls_dream_metadata" /> </service> - <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity" + <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity" android:exported="false" android:theme="@style/ShortcutHelperTheme" android:excludeFromRecents="true" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index de090f4b2a6d..21881f657e6d 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -171,16 +171,6 @@ flag { } flag { - name: "nssl_falsing_fix" - namespace: "systemui" - description: "Minor touch changes to prevent falsing errors in NSSL" - bug: "316551193" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "refactor_get_current_user" namespace: "systemui" description: "KeyguardUpdateMonitor.getCurrentUser() was providing outdated results." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt index 3eb1b14e72ba..604b517e19e5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt @@ -35,6 +35,7 @@ data class DisplayCutout( val viewDisplayCutoutKeyguardStatusBarView: ViewDisplayCutout? = null, ) { fun width() = abs(right.value - left.value).dp + fun height() = abs(bottom.value - top.value).dp } enum class CutoutLocation { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 79b57ca74f7d..3227611b841d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -804,6 +804,8 @@ private fun CommunalContent( is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier) is CommunalContentModel.WidgetContent.DisabledWidget -> DisabledWidgetPlaceholder(model, viewModel, modifier) + is CommunalContentModel.WidgetContent.PendingWidget -> + PendingWidgetPlaceholder(model, modifier) is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier) is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier) is CommunalContentModel.Tutorial -> TutorialContent(modifier) @@ -929,36 +931,36 @@ private fun WidgetContent( Modifier.semantics { contentDescription = accessibilityLabel onClick(label = clickActionLabel, action = null) - val deleteAction = - CustomAccessibilityAction(removeWidgetActionLabel) { - contentListState.onRemove(index) + val deleteAction = + CustomAccessibilityAction(removeWidgetActionLabel) { + contentListState.onRemove(index) + contentListState.onSaveList() + true + } + val selectWidgetAction = + CustomAccessibilityAction(clickActionLabel) { + val currentWidgetKey = + index?.let { + keyAtIndexIfEditable(contentListState.list, index) + } + viewModel.setSelectedKey(currentWidgetKey) + true + } + + val actions = mutableListOf(deleteAction, selectWidgetAction) + + if (selectedIndex != null && selectedIndex != index) { + actions.add( + CustomAccessibilityAction(placeWidgetActionLabel) { + contentListState.onMove(selectedIndex!!, index) contentListState.onSaveList() + viewModel.setSelectedKey(null) true } - val selectWidgetAction = - CustomAccessibilityAction(clickActionLabel) { - val currentWidgetKey = - index?.let { - keyAtIndexIfEditable(contentListState.list, index) - } - viewModel.setSelectedKey(currentWidgetKey) - true - } - - val actions = mutableListOf(deleteAction, selectWidgetAction) - - if (selectedIndex != null && selectedIndex != index) { - actions.add( - CustomAccessibilityAction(placeWidgetActionLabel) { - contentListState.onMove(selectedIndex!!, index) - contentListState.onSaveList() - viewModel.setSelectedKey(null) - true - } - ) - } + ) + } - customActions = actions + customActions = actions } } ) { @@ -1074,13 +1076,43 @@ fun DisabledWidgetPlaceholder( Image( painter = rememberDrawablePainter(icon.loadDrawable(context)), contentDescription = stringResource(R.string.icon_description_for_disabled_widget), - modifier = Modifier.size(48.dp), + modifier = Modifier.size(Dimensions.IconSize), colorFilter = ColorFilter.colorMatrix(Colors.DisabledColorFilter), ) } } @Composable +fun PendingWidgetPlaceholder( + model: CommunalContentModel.WidgetContent.PendingWidget, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val icon: Icon = + if (model.icon != null) { + Icon.createWithBitmap(model.icon) + } else { + Icon.createWithResource(context, android.R.drawable.sym_def_app_icon) + } + + Column( + modifier = + modifier.background( + MaterialTheme.colorScheme.surfaceVariant, + RoundedCornerShape(dimensionResource(system_app_widget_background_radius)) + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = rememberDrawablePainter(icon.loadDrawable(context)), + contentDescription = stringResource(R.string.icon_description_for_pending_widget), + modifier = Modifier.size(Dimensions.IconSize), + ) + } +} + +@Composable private fun SmartspaceContent( model: CommunalContentModel.Smartspace, modifier: Modifier = Modifier, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index ec9136d50e4c..bca8fde17fce 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -320,7 +320,6 @@ private fun SceneScope.QuickSettingsScene( createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, - modifier = Modifier.padding(horizontal = 16.dp), ) } Spacer(modifier = Modifier.height(16.dp)) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 7eaebc21355d..ff9c5a5ee586 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -5,10 +5,13 @@ import com.android.compose.animation.scene.transitions import com.android.systemui.bouncer.ui.composable.Bouncer import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly +import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition +import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition @@ -38,6 +41,13 @@ val SceneContainerTransitions = transitions { from( Scenes.Gone, to = Scenes.Shade, + key = GoneToSplitShade, + ) { + goneToSplitShadeTransition() + } + from( + Scenes.Gone, + to = Scenes.Shade, key = SlightlyFasterShadeCollapse, ) { goneToShadeTransition(durationScale = 0.9) @@ -68,5 +78,9 @@ val SceneContainerTransitions = transitions { Notifications.Elements.NotificationScrim, y = { Shade.Dimensions.ScrimOverscrollLimit } ) + translate( + Shade.Elements.SplitShadeStartColumn, + y = { Shade.Dimensions.ScrimOverscrollLimit } + ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt new file mode 100644 index 000000000000..4dc36d6f1878 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt @@ -0,0 +1,65 @@ +/* + * 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.composable.transitions + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.IntSize +import com.android.compose.animation.scene.TransitionBuilder +import com.android.compose.animation.scene.UserActionDistance +import com.android.compose.animation.scene.UserActionDistanceScope +import com.android.systemui.notifications.ui.composable.Notifications +import com.android.systemui.qs.ui.composable.QuickSettings +import com.android.systemui.shade.ui.composable.Shade +import com.android.systemui.shade.ui.composable.ShadeHeader +import kotlin.time.Duration.Companion.milliseconds + +fun TransitionBuilder.goneToSplitShadeTransition( + durationScale: Double = 1.0, +) { + spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) + swipeSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, + ) + distance = + object : UserActionDistance { + override fun UserActionDistanceScope.absoluteDistance( + fromSceneSize: IntSize, + orientation: Orientation, + ): Float { + return fromSceneSize.height.toFloat() * 2 / 3f + } + } + + fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) } + + fractionRange(start = .33f) { + fade(ShadeHeader.Elements.Clock) + fade(ShadeHeader.Elements.CollapsedContentStart) + fade(ShadeHeader.Elements.CollapsedContentEnd) + fade(ShadeHeader.Elements.PrivacyChip) + fade(QuickSettings.Elements.SplitShadeQuickSettings) + fade(QuickSettings.Elements.FooterActions) + fade(Notifications.Elements.NotificationScrim) + } +} + +private val DefaultDuration = 500.milliseconds diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index 0bd38a1daf43..709a416c0366 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexScenePicker @@ -63,6 +64,7 @@ import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout +import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.res.R @@ -77,6 +79,7 @@ import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel import com.android.systemui.statusbar.policy.Clock +import kotlin.math.max object ShadeHeader { object Elements { @@ -121,7 +124,11 @@ fun SceneScope.CollapsedShadeHeader( } val cutoutWidth = LocalDisplayCutout.current.width() + val cutoutHeight = LocalDisplayCutout.current.height() + val cutoutTop = LocalDisplayCutout.current.top val cutoutLocation = LocalDisplayCutout.current.location + val horizontalPadding = + max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) val useExpandedFormat by remember(cutoutLocation) { @@ -140,7 +147,7 @@ fun SceneScope.CollapsedShadeHeader( contents = listOf( { - Row { + Row(modifier = Modifier.padding(horizontal = horizontalPadding)) { Clock( scale = 1f, viewModel = viewModel, @@ -157,7 +164,12 @@ fun SceneScope.CollapsedShadeHeader( }, { if (isPrivacyChipVisible) { - Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) { + Box( + modifier = + Modifier.height(CollapsedHeight) + .fillMaxWidth() + .padding(horizontal = horizontalPadding) + ) { PrivacyChip( viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd), @@ -166,9 +178,13 @@ fun SceneScope.CollapsedShadeHeader( } else { Row( horizontalArrangement = Arrangement.End, - modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) + modifier = + Modifier.element(ShadeHeader.Elements.CollapsedContentEnd) + .padding(horizontal = horizontalPadding) ) { - SystemIconContainer { + SystemIconContainer( + modifier = Modifier.align(Alignment.CenterVertically) + ) { when (LocalWindowSizeClass.current.widthSizeClass) { WindowWidthSizeClass.Medium, WindowWidthSizeClass.Expanded -> @@ -206,7 +222,7 @@ fun SceneScope.CollapsedShadeHeader( val screenWidth = constraints.maxWidth val cutoutWidthPx = cutoutWidth.roundToPx() - val height = CollapsedHeight.roundToPx() + val height = max(cutoutHeight + (cutoutTop * 2), CollapsedHeight).roundToPx() val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height) val startMeasurable = measurables[0][0] diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 4ad8b9f47f70..36b60d64399a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -67,6 +67,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState import com.android.compose.modifiers.padding import com.android.compose.modifiers.thenIf import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.controls.ui.controller.MediaCarouselController @@ -97,6 +98,7 @@ object Shade { val MediaCarousel = ElementKey("ShadeMediaCarousel") val BackgroundScrim = ElementKey("ShadeBackgroundScrim", scenePicker = LowestZIndexScenePicker) + val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn") } object Dimensions { @@ -250,10 +252,6 @@ private fun SceneScope.SingleShade( createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, - modifier = - Modifier.padding( - horizontal = Shade.Dimensions.HorizontalPadding - ) ) Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) { QuickSettings( @@ -312,6 +310,8 @@ private fun SceneScope.SplitShade( modifier: Modifier = Modifier, shadeSession: SaveableSession, ) { + val screenCornerRadius = LocalScreenCornerRadius.current + val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState() val customizingAnimationDuration by @@ -320,7 +320,11 @@ private fun SceneScope.SplitShade( val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } val tileSquishiness by - animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness) + animateSceneFloatAsState( + value = 1f, + key = QuickSettings.SharedValues.TilesSquishiness, + canOverflow = false, + ) val unfoldTranslationXForStartSide by viewModel .unfoldTranslationX( @@ -388,8 +392,7 @@ private fun SceneScope.SplitShade( createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, modifier = - Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) - .then(brightnessMirrorShowingModifier) + Modifier.then(brightnessMirrorShowingModifier) .padding( horizontal = { unfoldTranslationXForStartSide.roundToInt() }, ) @@ -398,9 +401,9 @@ private fun SceneScope.SplitShade( Row(modifier = Modifier.fillMaxWidth().weight(1f)) { Box( modifier = - Modifier.weight(1f).graphicsLayer { - translationX = unfoldTranslationXForStartSide - }, + Modifier.element(Shade.Elements.SplitShadeStartColumn) + .weight(1f) + .graphicsLayer { translationX = unfoldTranslationXForStartSide }, ) { BrightnessMirror( viewModel = viewModel.brightnessMirrorViewModel, @@ -467,7 +470,7 @@ private fun SceneScope.SplitShade( modifier = Modifier.weight(1f) .fillMaxHeight() - .padding(bottom = navBarBottomHeight) + .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight) .then(brightnessMirrorShowingModifier) ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt index 0f6d51d3e3ea..79d17efcacc1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.anc.ui.composable +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -35,7 +36,6 @@ import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.systemui.res.R import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel @@ -81,11 +81,10 @@ constructor( onClick = onClick, ) Text( - modifier = Modifier.clearAndSetSemantics {}, + modifier = Modifier.clearAndSetSemantics {}.basicMarquee(), text = label, style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + maxLines = 2, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index c51e8b0ead12..a602e25e05c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -28,7 +28,11 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import com.android.systemui.res.R import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel @@ -49,6 +53,7 @@ fun VolumePanelRoot( } } + val accessibilityTitle = stringResource(R.string.accessibility_volume_settings) val state: VolumePanelState by viewModel.volumePanelState.collectAsState() val components by viewModel.componentsLayout.collectAsState(null) @@ -56,12 +61,14 @@ fun VolumePanelRoot( components?.let { componentsState -> Components( componentsState, - modifier.padding( - start = padding, - top = padding, - end = padding, - bottom = 20.dp, - ) + modifier + .semantics { paneTitle = accessibilityTitle } + .padding( + start = padding, + top = padding, + end = padding, + bottom = 20.dp, + ) ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 447c28067200..6c3f3c181d45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -17,8 +17,11 @@ package com.android.keyguard +import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags import android.content.res.Configuration import android.media.AudioManager +import android.platform.test.annotations.EnableFlags import android.telephony.TelephonyManager import android.testing.TestableLooper.RunWithLooper import android.testing.TestableResources @@ -148,6 +151,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Captor private lateinit var swipeListenerArgumentCaptor: @@ -273,6 +277,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, + devicePolicyManager, keyguardTransitionInteractor, { primaryBouncerInteractor }, ) { @@ -934,6 +939,45 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any()) } + @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() { + val mainUserId = 10 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ mainUserId, + /* expiringUserId = */ mainUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY)) + } + + @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() { + val secondaryUserId = 10 + val mainUserId = 0 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ secondaryUserId, + /* expiringUserId = */ secondaryUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog( + any(), + any(), + eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER) + ) + } + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 81878aaf4a18..0c5e726e17aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.authentication.domain.interactor import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -32,6 +34,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -410,12 +414,16 @@ class AuthenticationInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + kosmos.fakeUserRepository.asMainUser() + kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe = + FakeUserRepository.MAIN_USER_ID underTest.authenticate(correctPin) assertThat(upcomingWipe).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt index 23869576a7a4..7628debf245a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt @@ -50,6 +50,7 @@ class PackageChangeRepositoryTest : SysuiTestCase() { @Mock private lateinit var context: Context @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var handler: Handler + @Mock private lateinit var packageInstallerMonitor: PackageInstallerMonitor private lateinit var repository: PackageChangeRepository private lateinit var updateMonitor: PackageUpdateMonitor @@ -60,19 +61,20 @@ class PackageChangeRepositoryTest : SysuiTestCase() { MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest) whenever(context.packageManager).thenReturn(packageManager) - repository = PackageChangeRepositoryImpl { user -> - updateMonitor = - PackageUpdateMonitor( - user = user, - bgDispatcher = testDispatcher, - scope = applicationCoroutineScope, - context = context, - bgHandler = handler, - logger = PackageUpdateLogger(logcatLogBuffer()), - systemClock = fakeSystemClock, - ) - updateMonitor - } + repository = + PackageChangeRepositoryImpl(packageInstallerMonitor) { user -> + updateMonitor = + PackageUpdateMonitor( + user = user, + bgDispatcher = testDispatcher, + scope = applicationCoroutineScope, + context = context, + bgHandler = handler, + logger = PackageUpdateLogger(logcatLogBuffer()), + systemClock = fakeSystemClock, + ) + updateMonitor + } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt new file mode 100644 index 000000000000..5556b04c2d20 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt @@ -0,0 +1,228 @@ +/* + * 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.common.data.repository + +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionInfo +import android.graphics.Bitmap +import android.os.fakeExecutorHandler +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.PackageInstallSession +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class PackageInstallerMonitorTest : SysuiTestCase() { + @Mock private lateinit var packageInstaller: PackageInstaller + @Mock private lateinit var icon1: Bitmap + @Mock private lateinit var icon2: Bitmap + @Mock private lateinit var icon3: Bitmap + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val handler = kosmos.fakeExecutorHandler + + private lateinit var session1: SessionInfo + private lateinit var session2: SessionInfo + private lateinit var session3: SessionInfo + + private lateinit var defaultSessions: List<SessionInfo> + + private lateinit var underTest: PackageInstallerMonitor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + session1 = + SessionInfo().apply { + sessionId = 1 + appPackageName = "pkg_name_1" + appIcon = icon1 + } + session2 = + SessionInfo().apply { + sessionId = 2 + appPackageName = "pkg_name_2" + appIcon = icon2 + } + session3 = + SessionInfo().apply { + sessionId = 3 + appPackageName = "pkg_name_3" + appIcon = icon3 + } + defaultSessions = listOf(session1, session2) + + whenever(packageInstaller.allSessions).thenReturn(defaultSessions) + whenever(packageInstaller.getSessionInfo(1)).thenReturn(session1) + whenever(packageInstaller.getSessionInfo(2)).thenReturn(session2) + + underTest = + PackageInstallerMonitor( + handler, + kosmos.applicationCoroutineScope, + logcatLogBuffer("PackageInstallerRepositoryImplTest"), + packageInstaller, + ) + } + + @Test + fun installSessions_callbacksRegisteredOnlyWhenFlowIsCollected() = + testScope.runTest { + // Verify callback not added before flow is collected + verify(packageInstaller, never()).registerSessionCallback(any(), eq(handler)) + + // Start collecting the flow + val job = + backgroundScope.launch { + underTest.installSessionsForPrimaryUser.collect { + // Do nothing with the value + } + } + runCurrent() + + // Verify callback added only after flow is collected + val callback = + withArgCaptor<PackageInstaller.SessionCallback> { + verify(packageInstaller).registerSessionCallback(capture(), eq(handler)) + } + + // Verify callback not removed + verify(packageInstaller, never()).unregisterSessionCallback(any()) + + // Stop collecting the flow + job.cancel() + runCurrent() + + // Verify callback removed once flow stops being collected + verify(packageInstaller).unregisterSessionCallback(eq(callback)) + } + + @Test + fun installSessions_newSessionsAreAdded() = + testScope.runTest { + val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser) + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions) + + val callback = + withArgCaptor<PackageInstaller.SessionCallback> { + verify(packageInstaller).registerSessionCallback(capture(), eq(handler)) + } + + // New session added + whenever(packageInstaller.getSessionInfo(3)).thenReturn(session3) + callback.onCreated(3) + runCurrent() + + // Verify flow updated with the new session + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions + session3) + } + + @Test + fun installSessions_finishedSessionsAreRemoved() = + testScope.runTest { + val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser) + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions) + + val callback = + withArgCaptor<PackageInstaller.SessionCallback> { + verify(packageInstaller).registerSessionCallback(capture(), eq(handler)) + } + + // Session 1 finished successfully + callback.onFinished(1, /* success = */ true) + runCurrent() + + // Verify flow updated with session 1 removed + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions - session1) + } + + @Test + fun installSessions_sessionsUpdatedOnBadgingChange() = + testScope.runTest { + val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser) + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions) + + val callback = + withArgCaptor<PackageInstaller.SessionCallback> { + verify(packageInstaller).registerSessionCallback(capture(), eq(handler)) + } + + // App icon for session 1 updated + val newSession = + SessionInfo().apply { + sessionId = 1 + appPackageName = "pkg_name_1" + appIcon = mock() + } + whenever(packageInstaller.getSessionInfo(1)).thenReturn(newSession) + callback.onBadgingChanged(1) + runCurrent() + + // Verify flow updated with the new session 1 + assertThat(installSessions) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(defaultSessions - session1 + newSession) + } + + private val represents = + Correspondence.from<PackageInstallSession, SessionInfo>( + { actual, expected -> + actual?.sessionId == expected?.sessionId && + actual?.packageName == expected?.appPackageName && + actual?.icon == expected?.getAppIcon() + }, + "represents", + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index fe4d32d88612..6ce6cdb32a12 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -17,16 +17,18 @@ package com.android.systemui.communal.data.repository import android.app.backup.BackupManager -import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName import android.content.applicationContext +import android.graphics.Bitmap import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.fakePackageChangeRepository +import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao @@ -46,10 +48,10 @@ import com.android.systemui.log.logcatLogBuffer import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat -import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent @@ -58,7 +60,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.anyInt import org.mockito.Mockito.eq import org.mockito.Mockito.never import org.mockito.Mockito.verify @@ -68,10 +69,10 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalWidgetRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var appWidgetManager: AppWidgetManager @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost - @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo @Mock private lateinit var providerInfoA: AppWidgetProviderInfo + @Mock private lateinit var providerInfoB: AppWidgetProviderInfo + @Mock private lateinit var providerInfoC: AppWidgetProviderInfo @Mock private lateinit var communalWidgetHost: CommunalWidgetHost @Mock private lateinit var communalWidgetDao: CommunalWidgetDao @Mock private lateinit var backupManager: BackupManager @@ -79,9 +80,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private lateinit var backupUtils: CommunalBackupUtils private lateinit var logBuffer: LogBuffer private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> + private lateinit var fakeProviders: MutableStateFlow<Map<Int, AppWidgetProviderInfo?>> private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val packageChangeRepository = kosmos.fakePackageChangeRepository private val fakeAllowlist = listOf( @@ -96,6 +99,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) fakeWidgets = MutableStateFlow(emptyMap()) + fakeProviders = MutableStateFlow(emptyMap()) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") backupUtils = CommunalBackupUtils(kosmos.applicationContext) @@ -103,12 +107,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) - whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets) + whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders) underTest = CommunalWidgetRepositoryImpl( - Optional.of(appWidgetManager), appWidgetHost, testScope.backgroundScope, kosmos.testDispatcher, @@ -117,6 +120,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { logBuffer, backupManager, backupUtils, + packageChangeRepository, ) } @@ -126,15 +130,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) - whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) - - installedProviders(listOf(stopwatchProviderInfo)) + fakeProviders.value = mapOf(1 to providerInfoA) val communalWidgets by collectLastValue(underTest.communalWidgets) verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = communalWidgetItemEntry.widgetId, providerInfo = providerInfoA, priority = communalItemRankEntry.rank, @@ -146,6 +148,102 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test + fun communalWidgets_widgetsWithoutMatchingProvidersAreSkipped() = + testScope.runTest { + // Set up 4 widgets, but widget 3 and 4 don't have matching providers + fakeWidgets.value = + mapOf( + CommunalItemRank(uid = 1L, rank = 1) to + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalItemRank(uid = 2L, rank = 2) to + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + CommunalItemRank(uid = 3L, rank = 3) to + CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L), + CommunalItemRank(uid = 4L, rank = 4) to + CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L), + ) + fakeProviders.value = + mapOf( + 1 to providerInfoA, + 2 to providerInfoB, + ) + + // Expect to see only widget 1 and 2 + val communalWidgets by collectLastValue(underTest.communalWidgets) + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Available( + appWidgetId = 1, + providerInfo = providerInfoA, + priority = 1, + ), + CommunalWidgetContentModel.Available( + appWidgetId = 2, + providerInfo = providerInfoB, + priority = 2, + ), + ) + } + + @Test + fun communalWidgets_updatedWhenProvidersUpdate() = + testScope.runTest { + // Set up widgets and providers + fakeWidgets.value = + mapOf( + CommunalItemRank(uid = 1L, rank = 1) to + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalItemRank(uid = 2L, rank = 2) to + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + ) + fakeProviders.value = + mapOf( + 1 to providerInfoA, + 2 to providerInfoB, + ) + + // Expect two widgets + val communalWidgets by collectLastValue(underTest.communalWidgets) + assertThat(communalWidgets).isNotNull() + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Available( + appWidgetId = 1, + providerInfo = providerInfoA, + priority = 1, + ), + CommunalWidgetContentModel.Available( + appWidgetId = 2, + providerInfo = providerInfoB, + priority = 2, + ), + ) + + // Provider info updated for widget 1 + fakeProviders.value = + mapOf( + 1 to providerInfoC, + 2 to providerInfoB, + ) + runCurrent() + + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Available( + appWidgetId = 1, + // Verify that provider info updated + providerInfo = providerInfoC, + priority = 1, + ), + CommunalWidgetContentModel.Available( + appWidgetId = 2, + providerInfo = providerInfoB, + priority = 2, + ), + ) + } + + @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { val provider = ComponentName("pkg_name", "cls_name") @@ -434,9 +532,102 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank) } - private fun installedProviders(providers: List<AppWidgetProviderInfo>) { - whenever(appWidgetManager.installedProviders).thenReturn(providers) - } + @Test + fun pendingWidgets() = + testScope.runTest { + fakeWidgets.value = + mapOf( + CommunalItemRank(uid = 1L, rank = 1) to + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + CommunalItemRank(uid = 2L, rank = 2) to + CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L), + ) + + // Widget 1 is installed + fakeProviders.value = mapOf(1 to providerInfoA) + + // Widget 2 is pending install + val fakeIcon = mock<Bitmap>() + packageChangeRepository.setInstallSessions( + listOf( + PackageInstallSession( + sessionId = 1, + packageName = "pk_2", + icon = fakeIcon, + user = UserHandle.CURRENT, + ) + ) + ) + + val communalWidgets by collectLastValue(underTest.communalWidgets) + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Available( + appWidgetId = 1, + providerInfo = providerInfoA, + priority = 1, + ), + CommunalWidgetContentModel.Pending( + appWidgetId = 2, + priority = 2, + packageName = "pk_2", + icon = fakeIcon, + user = UserHandle.CURRENT, + ), + ) + } + + @Test + fun pendingWidgets_pendingWidgetBecomesAvailableAfterInstall() = + testScope.runTest { + fakeWidgets.value = + mapOf( + CommunalItemRank(uid = 1L, rank = 1) to + CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L), + ) + + // Widget 1 is pending install + val fakeIcon = mock<Bitmap>() + packageChangeRepository.setInstallSessions( + listOf( + PackageInstallSession( + sessionId = 1, + packageName = "pk_1", + icon = fakeIcon, + user = UserHandle.CURRENT, + ) + ) + ) + + val communalWidgets by collectLastValue(underTest.communalWidgets) + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Pending( + appWidgetId = 1, + priority = 1, + packageName = "pk_1", + icon = fakeIcon, + user = UserHandle.CURRENT, + ), + ) + + // Package for widget 1 finished installing + packageChangeRepository.setInstallSessions(emptyList()) + + // Provider info for widget 1 becomes available + fakeProviders.value = mapOf(1 to providerInfoA) + + runCurrent() + + assertThat(communalWidgets) + .containsExactly( + CommunalWidgetContentModel.Available( + appWidgetId = 1, + providerInfo = providerInfoA, + priority = 1, + ), + ) + } private fun setAppWidgetIds(ids: List<Int>) { whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 456fb79d0536..766798c2c2c3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -23,6 +23,7 @@ import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo import android.content.Intent import android.content.pm.UserInfo +import android.graphics.Bitmap import android.os.UserHandle import android.os.UserManager import android.os.userManager @@ -871,7 +872,14 @@ class CommunalInteractorTest : SysuiTestCase() { // One widget is filtered out and the remaining two link to main user id. assertThat(checkNotNull(widgetContent).size).isEqualTo(2) widgetContent!!.forEachIndexed { _, model -> - assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id) + assertThat(model is CommunalContentModel.WidgetContent.Widget).isTrue() + assertThat( + (model as CommunalContentModel.WidgetContent.Widget) + .providerInfo + .profile + ?.identifier + ) + .isEqualTo(MAIN_USER_INFO.id) } } @@ -1037,9 +1045,9 @@ class CommunalInteractorTest : SysuiTestCase() { runCurrent() val widgetContent by collectLastValue(underTest.widgetContent) - // Given three widgets, and one of them is associated with work profile. + // One available work widget, one pending work widget, and one regular available widget. val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) - val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id) val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) val widgets = listOf(widget1, widget2, widget3) widgetRepository.setCommunalWidgets(widgets) @@ -1049,11 +1057,9 @@ class CommunalInteractorTest : SysuiTestCase() { DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL ) - // Widget under work profile is filtered out and the remaining two link to main user id. - assertThat(widgetContent).hasSize(2) - widgetContent!!.forEach { model -> - assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id) - } + // Widgets under work profile are filtered out. Only the regular widget remains. + assertThat(widgetContent).hasSize(1) + assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(3) } @Test @@ -1076,7 +1082,7 @@ class CommunalInteractorTest : SysuiTestCase() { val widgetContent by collectLastValue(underTest.widgetContent) // Given three widgets, and one of them is associated with work profile. val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) - val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id) val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) val widgets = listOf(widget1, widget2, widget3) widgetRepository.setCommunalWidgets(widgets) @@ -1086,10 +1092,11 @@ class CommunalInteractorTest : SysuiTestCase() { DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE ) - // Widget under work profile is available. + // Widgets under work profile are available. assertThat(widgetContent).hasSize(3) - assertThat(widgetContent!![0].providerInfo.profile?.identifier) - .isEqualTo(USER_INFO_WORK.id) + assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(1) + assertThat(widgetContent?.get(1)?.appWidgetId).isEqualTo(2) + assertThat(widgetContent?.get(2)?.appWidgetId).isEqualTo(3) } @Test @@ -1182,8 +1189,11 @@ class CommunalInteractorTest : SysuiTestCase() { ) } - private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel = - mock<CommunalWidgetContentModel> { + private fun createWidgetForUser( + appWidgetId: Int, + userId: Int + ): CommunalWidgetContentModel.Available = + mock<CommunalWidgetContentModel.Available> { whenever(this.appWidgetId).thenReturn(appWidgetId) val providerInfo = mock<AppWidgetProviderInfo>().apply { @@ -1193,11 +1203,27 @@ class CommunalInteractorTest : SysuiTestCase() { whenever(this.providerInfo).thenReturn(providerInfo) } + private fun createPendingWidgetForUser( + appWidgetId: Int, + priority: Int = 0, + packageName: String = "", + icon: Bitmap? = null, + userId: Int = 0, + ): CommunalWidgetContentModel.Pending { + return CommunalWidgetContentModel.Pending( + appWidgetId = appWidgetId, + priority = priority, + packageName = packageName, + icon = icon, + user = UserHandle(userId), + ) + } + private fun createWidgetWithCategory( appWidgetId: Int, category: Int ): CommunalWidgetContentModel = - mock<CommunalWidgetContentModel> { + mock<CommunalWidgetContentModel.Available> { whenever(this.appWidgetId).thenReturn(appWidgetId) val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category } whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt index 89a4c04015b6..b3a12a69d1af 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt @@ -28,6 +28,8 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,6 +38,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -96,4 +101,137 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { assertThat(appWidgetIdToRemove).isEqualTo(2) } + + @Test + fun observer_onHostStartListeningTriggeredWhileObserverActive() = + testScope.runTest { + // Observer added + val observer = mock<CommunalAppWidgetHost.Observer>() + underTest.addObserver(observer) + runCurrent() + + // Verify callback triggered + verify(observer, never()).onHostStartListening() + underTest.startListening() + runCurrent() + verify(observer).onHostStartListening() + + clearInvocations(observer) + + // Observer removed + underTest.removeObserver(observer) + runCurrent() + + // Verify callback not triggered + underTest.startListening() + runCurrent() + verify(observer, never()).onHostStartListening() + } + + @Test + fun observer_onHostStopListeningTriggeredWhileObserverActive() = + testScope.runTest { + // Observer added + val observer = mock<CommunalAppWidgetHost.Observer>() + underTest.addObserver(observer) + runCurrent() + + // Verify callback triggered + verify(observer, never()).onHostStopListening() + underTest.stopListening() + runCurrent() + verify(observer).onHostStopListening() + + clearInvocations(observer) + + // Observer removed + underTest.removeObserver(observer) + runCurrent() + + // Verify callback not triggered + underTest.stopListening() + runCurrent() + verify(observer, never()).onHostStopListening() + } + + @Test + fun observer_onAllocateAppWidgetIdTriggeredWhileObserverActive() = + testScope.runTest { + // Observer added + val observer = mock<CommunalAppWidgetHost.Observer>() + underTest.addObserver(observer) + runCurrent() + + // Verify callback triggered + verify(observer, never()).onAllocateAppWidgetId(any()) + val id = underTest.allocateAppWidgetId() + runCurrent() + verify(observer).onAllocateAppWidgetId(eq(id)) + + clearInvocations(observer) + + // Observer removed + underTest.removeObserver(observer) + runCurrent() + + // Verify callback not triggered + underTest.allocateAppWidgetId() + runCurrent() + verify(observer, never()).onAllocateAppWidgetId(any()) + } + + @Test + fun observer_onDeleteAppWidgetIdTriggeredWhileObserverActive() = + testScope.runTest { + // Observer added + val observer = mock<CommunalAppWidgetHost.Observer>() + underTest.addObserver(observer) + runCurrent() + + // Verify callback triggered + verify(observer, never()).onDeleteAppWidgetId(any()) + underTest.deleteAppWidgetId(1) + runCurrent() + verify(observer).onDeleteAppWidgetId(eq(1)) + + clearInvocations(observer) + + // Observer removed + underTest.removeObserver(observer) + runCurrent() + + // Verify callback not triggered + underTest.deleteAppWidgetId(2) + runCurrent() + verify(observer, never()).onDeleteAppWidgetId(any()) + } + + @Test + fun observer_multipleObservers() = + testScope.runTest { + // Set up two observers + val observer1 = mock<CommunalAppWidgetHost.Observer>() + val observer2 = mock<CommunalAppWidgetHost.Observer>() + underTest.addObserver(observer1) + underTest.addObserver(observer2) + runCurrent() + + // Verify both observers triggered + verify(observer1, never()).onHostStartListening() + verify(observer2, never()).onHostStartListening() + underTest.startListening() + runCurrent() + verify(observer1).onHostStartListening() + verify(observer2).onHostStartListening() + + // Observer 1 removed + underTest.removeObserver(observer1) + runCurrent() + + // Verify only observer 2 is triggered + underTest.stopListening() + runCurrent() + verify(observer2).onHostStopListening() + verify(observer1, never()).onHostStopListening() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 9aebc305fb56..6ca04dfca6a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -123,12 +123,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { // Widgets available. val widgets = listOf( - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 0, priority = 30, providerInfo = providerInfo, ), - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 1, priority = 20, providerInfo = providerInfo, @@ -177,12 +177,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { // Widgets available. val widgets = listOf( - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 0, priority = 30, providerInfo = providerInfo, ), - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 1, priority = 20, providerInfo = providerInfo, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 569116c6124a..9d6a66de367f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -186,12 +186,12 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { // Widgets available. val widgets = listOf( - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 0, priority = 30, providerInfo = providerInfo, ), - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 1, priority = 20, providerInfo = providerInfo, @@ -245,7 +245,7 @@ class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() { widgetRepository.setCommunalWidgets( listOf( - CommunalWidgetContentModel( + CommunalWidgetContentModel.Available( appWidgetId = 1, priority = 1, providerInfo = providerInfo, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index 6cae5d352fc2..3d2eabf2a07c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetProviderInfo import android.content.pm.UserInfo +import android.graphics.Bitmap import android.os.UserHandle import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -60,6 +61,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { private val kosmos = testKosmos() @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + @Mock private lateinit var communalWidgetHost: CommunalWidgetHost private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int> @@ -78,6 +80,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { underTest = CommunalAppWidgetHostStartable( appWidgetHost, + communalWidgetHost, kosmos.communalInteractor, kosmos.fakeUserTracker, kosmos.applicationCoroutineScope, @@ -143,16 +146,44 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } @Test + fun observeHostWhenCommunalIsAvailable() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(true) + communalInteractor.setEditModeOpen(false) + verify(communalWidgetHost, never()).startObservingHost() + verify(communalWidgetHost, never()).stopObservingHost() + + underTest.start() + runCurrent() + + verify(communalWidgetHost).startObservingHost() + verify(communalWidgetHost, never()).stopObservingHost() + + setCommunalAvailable(false) + runCurrent() + + verify(communalWidgetHost).stopObservingHost() + } + } + + @Test fun removeAppWidgetReportedByHost() = with(kosmos) { testScope.runTest { // Set up communal widgets val widget1 = - mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) } + mock<CommunalWidgetContentModel.Available> { + whenever(this.appWidgetId).thenReturn(1) + } val widget2 = - mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) } + mock<CommunalWidgetContentModel.Available> { + whenever(this.appWidgetId).thenReturn(2) + } val widget3 = - mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) } + mock<CommunalWidgetContentModel.Available> { + whenever(this.appWidgetId).thenReturn(3) + } fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3)) underTest.start() @@ -184,8 +215,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK), selectedUserIndex = 0, ) + // One work widget, one pending work widget, and one personal widget. val widget1 = createWidgetForUser(1, USER_INFO_WORK.id) - val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id) + val widget2 = createPendingWidgetForUser(2, USER_INFO_WORK.id) val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id) val widgets = listOf(widget1, widget2, widget3) fakeCommunalWidgetRepository.setCommunalWidgets(widgets) @@ -209,8 +241,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fakeKeyguardRepository.setKeyguardShowing(true) runCurrent() - // Widget created for work profile is removed. - assertThat(communalWidgets).containsExactly(widget2, widget3) + // Both work widgets are removed. + assertThat(communalWidgets).containsExactly(widget3) } } @@ -227,14 +259,32 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { ) } - private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel = - mock<CommunalWidgetContentModel> { + private fun createWidgetForUser( + appWidgetId: Int, + userId: Int + ): CommunalWidgetContentModel.Available = + mock<CommunalWidgetContentModel.Available> { whenever(this.appWidgetId).thenReturn(appWidgetId) val providerInfo = mock<AppWidgetProviderInfo>() whenever(providerInfo.profile).thenReturn(UserHandle(userId)) whenever(this.providerInfo).thenReturn(providerInfo) } + private fun createPendingWidgetForUser( + appWidgetId: Int, + userId: Int, + priority: Int = 0, + packageName: String = "", + icon: Bitmap? = null, + ): CommunalWidgetContentModel.Pending = + CommunalWidgetContentModel.Pending( + appWidgetId = appWidgetId, + priority = priority, + packageName = packageName, + icon = icon, + user = UserHandle(userId), + ) + private companion object { val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt index 88f5e1b85840..054e516db943 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.widgets +import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName @@ -26,6 +27,8 @@ 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.coroutines.collectValues +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos @@ -40,6 +43,7 @@ import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -47,6 +51,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -59,6 +65,10 @@ class CommunalWidgetHostTest : SysuiTestCase() { @Mock private lateinit var appWidgetManager: AppWidgetManager @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + @Mock private lateinit var providerInfo1: AppWidgetProviderInfo + @Mock private lateinit var providerInfo2: AppWidgetProviderInfo + @Mock private lateinit var providerInfo3: AppWidgetProviderInfo + private val selectedUserInteractor: SelectedUserInteractor by lazy { kosmos.selectedUserInteractor } @@ -69,8 +79,19 @@ class CommunalWidgetHostTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + whenever( + appWidgetManager.bindAppWidgetIdIfAllowed( + any<Int>(), + any<UserHandle>(), + any<ComponentName>(), + any<Bundle>() + ) + ) + .thenReturn(true) + underTest = CommunalWidgetHost( + kosmos.applicationCoroutineScope, Optional.of(appWidgetManager), appWidgetHost, selectedUserInteractor, @@ -89,15 +110,6 @@ class CommunalWidgetHostTest : SysuiTestCase() { val user = UserHandle(checkNotNull(userId)) whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId) - whenever( - appWidgetManager.bindAppWidgetIdIfAllowed( - any<Int>(), - any<UserHandle>(), - any<ComponentName>(), - any<Bundle>(), - ) - ) - .thenReturn(true) // bind the widget with the current user when no user is explicitly set val result = underTest.allocateIdAndBindWidget(provider) @@ -121,15 +133,6 @@ class CommunalWidgetHostTest : SysuiTestCase() { val user = UserHandle(0) whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId) - whenever( - appWidgetManager.bindAppWidgetIdIfAllowed( - any<Int>(), - any<UserHandle>(), - any<ComponentName>(), - any<Bundle>() - ) - ) - .thenReturn(true) // provider and user handle are both set val result = underTest.allocateIdAndBindWidget(provider, user) @@ -172,6 +175,261 @@ class CommunalWidgetHostTest : SysuiTestCase() { assertThat(result).isNull() } + @Test + fun listener_exactlyOneListenerRegisteredForEachWidgetWhenHostStartListening() = + testScope.runTest { + // 3 widgets registered with the host + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3)) + + underTest.startObservingHost() + runCurrent() + + // Make sure no listener is set before host starts listening + verify(appWidgetHost, never()).setListener(any(), any()) + + // Host starts listening + val observer = + withArgCaptor<CommunalAppWidgetHost.Observer> { + verify(appWidgetHost).addObserver(capture()) + } + observer.onHostStartListening() + runCurrent() + + // Verify a listener is set for each widget + verify(appWidgetHost, times(3)).setListener(any(), any()) + verify(appWidgetHost).setListener(eq(1), any()) + verify(appWidgetHost).setListener(eq(2), any()) + verify(appWidgetHost).setListener(eq(3), any()) + } + + @Test + fun listener_listenersRemovedWhenHostStopListening() = + testScope.runTest { + // 3 widgets registered with the host + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3)) + + underTest.startObservingHost() + runCurrent() + + // Host starts listening + val observer = + withArgCaptor<CommunalAppWidgetHost.Observer> { + verify(appWidgetHost).addObserver(capture()) + } + observer.onHostStartListening() + runCurrent() + + // Verify none of the listener is removed before host stop listening + verify(appWidgetHost, never()).removeListener(any()) + + observer.onHostStopListening() + + // Verify each listener is removed + verify(appWidgetHost, times(3)).removeListener(any()) + verify(appWidgetHost).removeListener(eq(1)) + verify(appWidgetHost).removeListener(eq(2)) + verify(appWidgetHost).removeListener(eq(3)) + } + + @Test + fun listener_addNewListenerWhenNewIdAllocated() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf()) + val observer = start() + + // Verify no listener is set before a new app widget id is allocated + verify(appWidgetHost, never()).setListener(any(), any()) + + // Allocate an app widget id + observer.onAllocateAppWidgetId(1) + + // Verify new listener set for that app widget id + verify(appWidgetHost).setListener(eq(1), any()) + } + + @Test + fun listener_removeListenerWhenWidgetDeleted() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1)) + val observer = start() + + // Verify listener not removed before widget deleted + verify(appWidgetHost, never()).removeListener(eq(1)) + + // Widget deleted + observer.onDeleteAppWidgetId(1) + + // Verify listener removed for that widget + verify(appWidgetHost).removeListener(eq(1)) + } + + @Test + fun providerInfo_populatesWhenStartListening() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2)) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2) + + val providerInfoValues by collectValues(underTest.appWidgetProviders) + + // Assert that the map is empty before host starts listening + assertThat(providerInfoValues).hasSize(1) + assertThat(providerInfoValues[0]).isEmpty() + + start() + runCurrent() + + // Assert that the provider info map is populated after host started listening, and that + // all providers are emitted at once + assertThat(providerInfoValues).hasSize(2) + assertThat(providerInfoValues[1]) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + ) + ) + } + + @Test + fun providerInfo_clearsWhenStopListening() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2)) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2) + + val observer = start() + runCurrent() + + // Assert that the provider info map is populated + val providerInfo by collectLastValue(underTest.appWidgetProviders) + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + ) + ) + + // Host stop listening + observer.onHostStopListening() + + // Assert that the provider info map is cleared + assertThat(providerInfo).isEmpty() + } + + @Test + fun providerInfo_onUpdate() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2)) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2) + + val providerInfo by collectLastValue(underTest.appWidgetProviders) + + start() + runCurrent() + + // Assert that the provider info map is populated + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + ) + ) + + // Provider info for widget 1 updated + val listener = + withArgCaptor<AppWidgetHost.AppWidgetHostListener> { + verify(appWidgetHost).setListener(eq(1), capture()) + } + listener.onUpdateProviderInfo(providerInfo3) + runCurrent() + + // Assert that the update is reflected in the flow + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo3), + Pair(2, providerInfo2), + ) + ) + } + + @Test + fun providerInfo_updateWhenANewWidgetIsBound() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2)) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2) + + val providerInfo by collectLastValue(underTest.appWidgetProviders) + + start() + runCurrent() + + // Assert that the provider info map is populated + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + ) + ) + + // Bind a new widget + whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(3) + whenever(appWidgetManager.getAppWidgetInfo(3)).thenReturn(providerInfo3) + val newWidgetComponentName = ComponentName.unflattenFromString("pkg_new/cls_new")!! + underTest.allocateIdAndBindWidget(newWidgetComponentName) + runCurrent() + + // Assert that the new provider is reflected in the flow + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + Pair(3, providerInfo3), + ) + ) + } + + @Test + fun providerInfo_updateWhenWidgetRemoved() = + testScope.runTest { + whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2)) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2) + + val providerInfo by collectLastValue(underTest.appWidgetProviders) + + val observer = start() + runCurrent() + + // Assert that the provider info map is populated + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(1, providerInfo1), + Pair(2, providerInfo2), + ) + ) + + // Remove widget 1 + observer.onDeleteAppWidgetId(1) + runCurrent() + + // Assert that provider info for widget 1 is removed + assertThat(providerInfo) + .containsExactlyEntriesIn( + mapOf( + Pair(2, providerInfo2), + ) + ) + } + private fun selectUser() { kosmos.fakeUserRepository.selectedUser.value = SelectedUserModel( @@ -179,4 +437,16 @@ class CommunalWidgetHostTest : SysuiTestCase() { selectionStatus = SelectionStatus.SELECTION_COMPLETE ) } + + private fun TestScope.start(): CommunalAppWidgetHost.Observer { + underTest.startObservingHost() + runCurrent() + + val observer = + withArgCaptor<CommunalAppWidgetHost.Observer> { + verify(appWidgetHost).addObserver(capture()) + } + observer.onHostStartListening() + return observer + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt new file mode 100644 index 000000000000..1e5599bfe1d5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt @@ -0,0 +1,165 @@ +/* + * 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.qs.panels.data.repository + +import android.content.ComponentName +import android.content.packageManager +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.content.pm.UserInfo +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.kosmos.mainCoroutineContext +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.panels.shared.model.EditTileData +import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository +import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository +import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +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 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class IconAndNameCustomRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val packageManager: PackageManager = kosmos.packageManager + private val userTracker: FakeUserTracker = + kosmos.fakeUserTracker.apply { + whenever(userContext.packageManager).thenReturn(packageManager) + } + + private val service1 = + FakeInstalledTilesComponentRepository.ServiceInfo( + component1, + tileService1, + drawable1, + appName1, + ) + + private val service2 = + FakeInstalledTilesComponentRepository.ServiceInfo( + component2, + tileService2, + drawable2, + appName2, + ) + + private val underTest = + with(kosmos) { + IconAndNameCustomRepository( + installedTilesRepository, + userTracker, + mainCoroutineContext, + ) + } + + @Before + fun setUp() { + kosmos.fakeInstalledTilesRepository.setInstalledServicesForUser( + userTracker.userId, + listOf(service1, service2) + ) + } + + @Test + fun loadDataForCurrentServices() = + with(kosmos) { + testScope.runTest { + val editTileDataList = underTest.getCustomTileData() + val expectedData1 = + EditTileData( + TileSpec.create(component1), + Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)), + Text.Loaded(tileService1), + Text.Loaded(appName1), + ) + val expectedData2 = + EditTileData( + TileSpec.create(component2), + Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)), + Text.Loaded(tileService2), + Text.Loaded(appName2), + ) + + assertThat(editTileDataList).containsExactly(expectedData1, expectedData2) + } + } + + @Test + fun loadDataForCurrentServices_otherCurrentUser_empty() = + with(kosmos) { + testScope.runTest { + userTracker.set(listOf(UserInfo(11, "", 0)), 0) + val editTileDataList = underTest.getCustomTileData() + + assertThat(editTileDataList).isEmpty() + } + } + + @Test + fun loadDataForCurrentServices_serviceInfoWithNullIcon_notInList() = + with(kosmos) { + testScope.runTest { + val serviceNullIcon = + FakeInstalledTilesComponentRepository.ServiceInfo( + component2, + tileService2, + ) + fakeInstalledTilesRepository.setInstalledServicesForUser( + userTracker.userId, + listOf(service1, serviceNullIcon) + ) + + val expectedData1 = + EditTileData( + TileSpec.create(component1), + Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)), + Text.Loaded(tileService1), + Text.Loaded(appName1), + ) + + val editTileDataList = underTest.getCustomTileData() + assertThat(editTileDataList).containsExactly(expectedData1) + } + } + + private companion object { + val drawable1 = TestStubDrawable("drawable1") + val appName1 = "App1" + val tileService1 = "Tile Service 1" + val component1 = ComponentName("pkg1", "srv1") + + val drawable2 = TestStubDrawable("drawable2") + val appName2 = "App2" + val tileService2 = "Tile Service 2" + val component2 = ComponentName("pkg2", "srv2") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt new file mode 100644 index 000000000000..56cead19d1df --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt @@ -0,0 +1,46 @@ +/* + * 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.qs.panels.data.repository + +import android.content.res.mainResources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class StockTilesRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private val underTest = StockTilesRepository(kosmos.mainResources) + + @Test + fun stockTilesMatchesResources() { + val expected = + kosmos.mainResources + .getString(R.string.quick_settings_tiles_stock) + .split(",") + .map(TileSpec::create) + assertThat(underTest.stockTiles).isEqualTo(expected) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt new file mode 100644 index 000000000000..deefbf585ba9 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt @@ -0,0 +1,198 @@ +/* + * 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.qs.panels.domain.interactor + +import android.content.ComponentName +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository +import com.android.systemui.qs.panels.data.repository.stockTilesRepository +import com.android.systemui.qs.panels.shared.model.EditTileData +import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository +import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig +import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig +import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider +import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider +import com.android.systemui.settings.userTracker +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 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class EditTilesListInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + + // Only have some configurations so we can test the effect of missing configurations. + // As the configurations are injected by dagger, we'll have all the existing configurations + private val internetTileConfig = kosmos.qsInternetTileConfig + private val flashlightTileConfig = kosmos.qsFlashlightTileConfig + private val batteryTileConfig = kosmos.qsBatterySaverTileConfig + + private val serviceInfo = + FakeInstalledTilesComponentRepository.ServiceInfo( + component, + tileName, + icon, + appName, + ) + + private val underTest = + with(kosmos) { + EditTilesListInteractor( + stockTilesRepository, + qSTileConfigProvider, + iconAndNameCustomRepository, + ) + } + + @Before + fun setUp() { + with(kosmos) { + fakeInstalledTilesRepository.setInstalledServicesForUser( + userTracker.userId, + listOf(serviceInfo) + ) + + with(fakeQSTileConfigProvider) { + putConfig(internetTileConfig.tileSpec, internetTileConfig) + putConfig(flashlightTileConfig.tileSpec, flashlightTileConfig) + putConfig(batteryTileConfig.tileSpec, batteryTileConfig) + } + } + } + + @Test + fun getTilesToEdit_stockTilesHaveNoAppName() = + with(kosmos) { + testScope.runTest { + val editTiles = underTest.getTilesToEdit() + + assertThat(editTiles.stockTiles.all { it.appName == null }).isTrue() + } + } + + @Test + fun getTilesToEdit_stockTilesAreAllPlatformSpecs() = + with(kosmos) { + testScope.runTest { + val editTiles = underTest.getTilesToEdit() + + assertThat(editTiles.stockTiles.all { it.tileSpec is TileSpec.PlatformTileSpec }) + .isTrue() + } + } + + @Test + fun getTilesToEdit_stockTiles_sameOrderAsRepository() = + with(kosmos) { + testScope.runTest { + val editTiles = underTest.getTilesToEdit() + + assertThat(editTiles.stockTiles.map { it.tileSpec }) + .isEqualTo(stockTilesRepository.stockTiles) + } + } + + @Test + fun getTilesToEdit_customTileData_matchesService() = + with(kosmos) { + testScope.runTest { + val editTiles = underTest.getTilesToEdit() + val expected = + EditTileData( + tileSpec = TileSpec.create(component), + icon = Icon.Loaded(icon, ContentDescription.Loaded(tileName)), + label = Text.Loaded(tileName), + appName = Text.Loaded(appName), + ) + + assertThat(editTiles.customTiles).hasSize(1) + assertThat(editTiles.customTiles[0]).isEqualTo(expected) + } + } + + @Test + fun getTilesToEdit_tilesInConfigProvider_correctData() = + with(kosmos) { + testScope.runTest { + val editTiles = underTest.getTilesToEdit() + + assertThat( + editTiles.stockTiles.first { it.tileSpec == internetTileConfig.tileSpec } + ) + .isEqualTo(internetTileConfig.toEditTileData()) + assertThat( + editTiles.stockTiles.first { it.tileSpec == flashlightTileConfig.tileSpec } + ) + .isEqualTo(flashlightTileConfig.toEditTileData()) + assertThat(editTiles.stockTiles.first { it.tileSpec == batteryTileConfig.tileSpec }) + .isEqualTo(batteryTileConfig.toEditTileData()) + } + } + + @Test + fun getTilesToEdit_tilesNotInConfigProvider_useDefaultData() = + with(kosmos) { + testScope.runTest { + underTest + .getTilesToEdit() + .stockTiles + .filterNot { qSTileConfigProvider.hasConfig(it.tileSpec.spec) } + .forEach { assertThat(it).isEqualTo(it.tileSpec.missingConfigEditTileData()) } + } + } + + private companion object { + val component = ComponentName("pkg", "srv") + const val tileName = "Tile Service" + const val appName = "App" + val icon = TestStubDrawable("icon") + + fun TileSpec.missingConfigEditTileData(): EditTileData { + return EditTileData( + tileSpec = this, + icon = Icon.Resource(android.R.drawable.star_on, ContentDescription.Loaded(spec)), + label = Text.Loaded(spec), + appName = null + ) + } + + fun QSTileConfig.toEditTileData(): EditTileData { + return EditTileData( + tileSpec = tileSpec, + icon = + Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)), + label = Text.Resource(uiConfig.labelRes), + appName = null, + ) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt new file mode 100644 index 000000000000..9fb25a28415b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt @@ -0,0 +1,507 @@ +/* + * 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.qs.panels.ui.viewmodel + +import android.R +import android.content.ComponentName +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.FakeQSTile +import com.android.systemui.qs.panels.data.repository.stockTilesRepository +import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor +import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap +import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor +import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout +import com.android.systemui.qs.panels.shared.model.EditTileData +import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository +import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository +import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository +import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository +import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.qsTileFactory +import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig +import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig +import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig +import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig +import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig +import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider +import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider +import com.android.systemui.settings.userTracker +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 + +@RunWith(AndroidJUnit4::class) +@SmallTest +class EditModeViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + + // Only have some configurations so we can test the effect of missing configurations. + // As the configurations are injected by dagger, we'll have all the existing configurations + private val configs = + with(kosmos) { + setOf( + qsInternetTileConfig, + qsFlashlightTileConfig, + qsBatterySaverTileConfig, + qsAlarmTileConfig, + qsCameraSensorPrivacyToggleTileConfig, + qsMicrophoneSensorPrivacyToggleTileConfig, + ) + } + + private val serviceInfo1 = + FakeInstalledTilesComponentRepository.ServiceInfo( + component1, + tileService1, + drawable1, + appName1, + ) + + private val serviceInfo2 = + FakeInstalledTilesComponentRepository.ServiceInfo( + component2, + tileService2, + drawable2, + appName2, + ) + + private val underTest: EditModeViewModel by lazy { + with(kosmos) { + EditModeViewModel( + editTilesListInteractor, + currentTilesInteractor, + minimumTilesInteractor, + infiniteGridLayout, + applicationCoroutineScope, + gridLayoutTypeInteractor, + gridLayoutMap, + ) + } + } + + @Before + fun setUp() { + with(kosmos) { + fakeMinimumTilesRepository = MinimumTilesFixedRepository(minNumberOfTiles) + + fakeInstalledTilesRepository.setInstalledServicesForUser( + userTracker.userId, + listOf(serviceInfo1, serviceInfo2) + ) + + with(fakeQSTileConfigProvider) { configs.forEach { putConfig(it.tileSpec, it) } } + qsTileFactory = FakeQSFactory { FakeQSTile(userTracker.userId, available = true) } + } + } + + @Test + fun isEditing() = + with(kosmos) { + testScope.runTest { + val isEditing by collectLastValue(underTest.isEditing) + + assertThat(isEditing).isFalse() + + underTest.startEditing() + assertThat(isEditing).isTrue() + + underTest.stopEditing() + assertThat(isEditing).isFalse() + } + } + + @Test + fun editing_false_emptyFlowOfTiles() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + assertThat(tiles).isNull() + } + } + + @Test + fun editing_true_notEmptyTileData() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + underTest.startEditing() + + assertThat(tiles).isNotEmpty() + } + } + + @Test + fun tilesData_hasAllStockTiles() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + underTest.startEditing() + + assertThat( + tiles!! + .filter { it.tileSpec is TileSpec.PlatformTileSpec } + .map { it.tileSpec } + ) + .containsExactlyElementsIn(stockTilesRepository.stockTiles) + } + } + + @Test + fun tilesData_stockTiles_haveCorrectUiValues() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + underTest.startEditing() + + tiles!! + .filter { it.tileSpec is TileSpec.PlatformTileSpec } + .forEach { + val data = getEditTileData(it.tileSpec) + + assertThat(it.label).isEqualTo(data.label) + assertThat(it.icon).isEqualTo(data.icon) + assertThat(it.appName).isNull() + } + } + } + + @Test + fun tilesData_hasAllCustomTiles() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + underTest.startEditing() + + assertThat( + tiles!! + .filter { it.tileSpec is TileSpec.CustomTileSpec } + .map { it.tileSpec } + ) + .containsExactly(TileSpec.create(component1), TileSpec.create(component2)) + } + } + + @Test + fun tilesData_customTiles_haveCorrectUiValues() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + underTest.startEditing() + + // service1 + val model1 = tiles!!.first { it.tileSpec == TileSpec.create(component1) } + assertThat(model1.label).isEqualTo(Text.Loaded(tileService1)) + assertThat(model1.appName).isEqualTo(Text.Loaded(appName1)) + assertThat(model1.icon) + .isEqualTo(Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1))) + + // service2 + val model2 = tiles!!.first { it.tileSpec == TileSpec.create(component2) } + assertThat(model2.label).isEqualTo(Text.Loaded(tileService2)) + assertThat(model2.appName).isEqualTo(Text.Loaded(appName2)) + assertThat(model2.icon) + .isEqualTo(Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2))) + } + } + + @Test + fun currentTiles_inCorrectOrder_markedAsCurrent() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + listOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec }) + .containsExactlyElementsIn(currentTiles) + .inOrder() + } + } + + @Test + fun notCurrentTiles() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + listOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + val remainingTiles = + stockTilesRepository.stockTiles.filterNot { it in currentTiles } + + listOf(TileSpec.create(component1)) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec }) + .containsExactlyElementsIn(remainingTiles) + } + } + + @Test + fun currentTilesChange_trackingChange() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + val newTile = TileSpec.create("internet") + val position = 1 + currentTilesInteractor.addTile(newTile, position) + currentTiles.add(position, newTile) + + assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec }) + .containsExactlyElementsIn(currentTiles) + .inOrder() + } + } + + @Test + fun nonCurrentTiles_orderPreservedWhenCurrentTilesChange() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + val nonCurrentSpecs = tiles!!.filterNot { it.isCurrent }.map { it.tileSpec } + val newTile = TileSpec.create("internet") + currentTilesInteractor.addTile(newTile) + + assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec }) + .containsExactlyElementsIn(nonCurrentSpecs - listOf(newTile)) + .inOrder() + } + } + + @Test + fun nonCurrentTiles_haveOnlyAddAction() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + tiles!! + .filterNot { it.isCurrent } + .forEach { + assertThat(it.availableEditActions) + .containsExactly(AvailableEditActions.ADD) + } + } + } + + @Test + fun currentTiles_moreThanMinimumTiles_haveRemoveAction() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + assertThat(currentTiles.size).isGreaterThan(minNumberOfTiles) + + underTest.startEditing() + + tiles!! + .filter { it.isCurrent } + .forEach { + assertThat(it.availableEditActions).contains(AvailableEditActions.REMOVE) + } + } + } + + @Test + fun currentTiles_minimumTiles_dontHaveRemoveAction() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + ) + currentTilesInteractor.setTiles(currentTiles) + assertThat(currentTiles.size).isEqualTo(minNumberOfTiles) + + underTest.startEditing() + + tiles!! + .filter { it.isCurrent } + .forEach { + assertThat(it.availableEditActions) + .doesNotContain(AvailableEditActions.REMOVE) + } + } + } + + @Test + fun currentTiles_lessThanMinimumTiles_dontHaveRemoveAction() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + ) + currentTilesInteractor.setTiles(currentTiles) + assertThat(currentTiles.size).isLessThan(minNumberOfTiles) + + underTest.startEditing() + + tiles!! + .filter { it.isCurrent } + .forEach { + assertThat(it.availableEditActions) + .doesNotContain(AvailableEditActions.REMOVE) + } + } + } + + @Test + fun currentTiles_haveMoveAction() = + with(kosmos) { + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + val currentTiles = + mutableListOf( + TileSpec.create("flashlight"), + TileSpec.create("airplane"), + TileSpec.create(component2), + TileSpec.create("alarm"), + ) + currentTilesInteractor.setTiles(currentTiles) + + underTest.startEditing() + + tiles!! + .filter { it.isCurrent } + .forEach { + assertThat(it.availableEditActions).contains(AvailableEditActions.MOVE) + } + } + } + + private companion object { + val drawable1 = TestStubDrawable("drawable1") + val appName1 = "App1" + val tileService1 = "Tile Service 1" + val component1 = ComponentName("pkg1", "srv1") + + val drawable2 = TestStubDrawable("drawable2") + val appName2 = "App2" + val tileService2 = "Tile Service 2" + val component2 = ComponentName("pkg2", "srv2") + + fun TileSpec.missingConfigEditTileData(): EditTileData { + return EditTileData( + tileSpec = this, + icon = Icon.Resource(R.drawable.star_on, ContentDescription.Loaded(spec)), + label = Text.Loaded(spec), + appName = null + ) + } + + fun QSTileConfig.toEditTileData(): EditTileData { + return EditTileData( + tileSpec = tileSpec, + icon = + Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)), + label = Text.Resource(uiConfig.labelRes), + appName = null, + ) + } + + fun Kosmos.getEditTileData(tileSpec: TileSpec): EditTileData { + return if (qSTileConfigProvider.hasConfig(tileSpec.spec)) { + qSTileConfigProvider.getConfig(tileSpec.spec).toEditTileData() + } else { + tileSpec.missingConfigEditTileData() + } + } + + val minNumberOfTiles = 3 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt index bc57ce6f95f5..a0dec8cb745d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt @@ -90,7 +90,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { } @Test - fun componentsLoadedOnStart() = + fun servicesLoadedOnStart() = testScope.runTest { val userId = 0 val resolveInfo = @@ -106,12 +106,14 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) runCurrent() + val services = underTest.getInstalledTilesServiceInfos(userId) assertThat(componentNames).containsExactly(TEST_COMPONENT) + assertThat(services).containsExactly(resolveInfo.serviceInfo) } @Test - fun componentAdded_foundAfterPackageChange() = + fun serviceAdded_foundAfterPackageChange() = testScope.runTest { val userId = 0 val resolveInfo = @@ -132,12 +134,14 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty) runCurrent() + val services = underTest.getInstalledTilesServiceInfos(userId) assertThat(componentNames).containsExactly(TEST_COMPONENT) + assertThat(services).containsExactly(resolveInfo.serviceInfo) } @Test - fun componentWithoutPermission_notValid() = + fun serviceWithoutPermission_notValid() = testScope.runTest { val userId = 0 val resolveInfo = @@ -152,13 +156,15 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + val services = underTest.getInstalledTilesServiceInfos(userId) runCurrent() assertThat(componentNames).isEmpty() + assertThat(services).isEmpty() } @Test - fun componentNotEnabled_notValid() = + fun serviceNotEnabled_notValid() = testScope.runTest { val userId = 0 val resolveInfo = @@ -173,9 +179,11 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + val services = underTest.getInstalledTilesServiceInfos(userId) runCurrent() assertThat(componentNames).isEmpty() + assertThat(services).isEmpty() } @Test @@ -221,30 +229,22 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) runCurrent() + val service = underTest.getInstalledTilesServiceInfos(userId) assertThat(componentNames).containsExactly(TEST_COMPONENT) + assertThat(service).containsExactly(resolveInfo.serviceInfo) } @Test - fun loadComponentsForSameUserTwice_returnsSameFlow() = + fun loadServicesForSameUserTwice_returnsSameFlow() = testScope.runTest { - val flowForUser1 = underTest.getInstalledTilesComponents(1) - val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1) + val flowForUser1 = underTest.getInstalledTilesServiceInfos(1) + val flowForUser1TheSecondTime = underTest.getInstalledTilesServiceInfos(1) runCurrent() assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1) } - @Test - fun loadComponentsForDifferentUsers_returnsDifferentFlow() = - testScope.runTest { - val flowForUser1 = underTest.getInstalledTilesComponents(1) - val flowForUser2 = underTest.getInstalledTilesComponents(2) - runCurrent() - - assertThat(flowForUser2).isNotEqualTo(flowForUser1) - } - // Tests that a ServiceInfo that is returned by queryIntentServicesAsUser but shortly // after uninstalled, doesn't crash SystemUI. @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt index 61e4774d9c92..3faab5048fb4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.domain.model.TileModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.ColorCorrectionTile diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt index 8ae917264a37..167eff193147 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.DumpManager +import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt index 634c5fa74295..1c73fe2b305d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.dump.nano.SystemUIProtoDump import com.android.systemui.plugins.qs.QSTile import com.android.systemui.plugins.qs.QSTile.BooleanState import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.external.CustomTile import com.android.systemui.qs.external.CustomTileStatePersister import com.android.systemui.qs.external.TileLifecycleManager diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt index 90c83047e72f..260189d401d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.FakeDefaultTilesRepository import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt index 4207a9c27ad0..dffd0d72969c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.FakeQSFactory +import com.android.systemui.qs.FakeQSTile import com.android.systemui.qs.pipeline.data.model.RestoreData import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository @@ -54,9 +55,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() { - private val kosmos by lazy { - Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } - } + private val kosmos by lazy { Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } } // Getter here so it can change when there is a managed profile. private val workTileAvailable: Boolean get() = hasManagedProfile() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt new file mode 100644 index 000000000000..a2ffe7008009 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt @@ -0,0 +1,82 @@ +/* + * 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.scene.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class GoneSceneViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val shadeRepository by lazy { kosmos.shadeRepository } + private lateinit var underTest: GoneSceneViewModel + + @Before + fun setUp() { + underTest = + GoneSceneViewModel( + applicationScope = testScope.backgroundScope, + shadeInteractor = kosmos.shadeInteractor, + ) + } + + @Test + fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + shadeRepository.setShadeMode(ShadeMode.Split) + runCurrent() + + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey) + .isEqualTo(GoneToSplitShade) + } + + @Test + fun downTransitionKey_splitShadeDisabled_isNull() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + shadeRepository.setShadeMode(ShadeMode.Single) + runCurrent() + + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 5312ad809a72..243921753f22 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -159,6 +160,27 @@ class ShadeSceneViewModelTest : SysuiTestCase() { } @Test + fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + shadeRepository.setShadeMode(ShadeMode.Split) + runCurrent() + + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey) + .isEqualTo(GoneToSplitShade) + } + + @Test + fun upTransitionKey_splitShadeDisabled_isNull() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + shadeRepository.setShadeMode(ShadeMode.Single) + runCurrent() + + assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull() + } + + @Test fun isClickable_deviceUnlocked_false() = testScope.runTest { val isClickable by collectLastValue(underTest.isClickable) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt index 03a39f8f07d8..2d8cd930d8fa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt @@ -23,9 +23,9 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos -import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not -import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -45,21 +45,21 @@ class BooleanFlowOperatorsTest : SysuiTestCase() { @Test fun and_allTrue_returnsTrue() = testScope.runTest { - val result by collectLastValue(and(TRUE, TRUE)) + val result by collectLastValue(allOf(TRUE, TRUE)) assertThat(result).isTrue() } @Test fun and_anyFalse_returnsFalse() = testScope.runTest { - val result by collectLastValue(and(TRUE, FALSE, TRUE)) + val result by collectLastValue(allOf(TRUE, FALSE, TRUE)) assertThat(result).isFalse() } @Test fun and_allFalse_returnsFalse() = testScope.runTest { - val result by collectLastValue(and(FALSE, FALSE, FALSE)) + val result by collectLastValue(allOf(FALSE, FALSE, FALSE)) assertThat(result).isFalse() } @@ -68,7 +68,7 @@ class BooleanFlowOperatorsTest : SysuiTestCase() { testScope.runTest { val flow1 = MutableStateFlow(false) val flow2 = MutableStateFlow(false) - val values by collectValues(and(flow1, flow2)) + val values by collectValues(allOf(flow1, flow2)) assertThat(values).containsExactly(false) flow1.value = true @@ -81,21 +81,21 @@ class BooleanFlowOperatorsTest : SysuiTestCase() { @Test fun or_allTrue_returnsTrue() = testScope.runTest { - val result by collectLastValue(or(TRUE, TRUE)) + val result by collectLastValue(anyOf(TRUE, TRUE)) assertThat(result).isTrue() } @Test fun or_anyTrue_returnsTrue() = testScope.runTest { - val result by collectLastValue(or(FALSE, TRUE, FALSE)) + val result by collectLastValue(anyOf(FALSE, TRUE, FALSE)) assertThat(result).isTrue() } @Test fun or_allFalse_returnsFalse() = testScope.runTest { - val result by collectLastValue(or(FALSE, FALSE, FALSE)) + val result by collectLastValue(anyOf(FALSE, FALSE, FALSE)) assertThat(result).isFalse() } @@ -104,7 +104,7 @@ class BooleanFlowOperatorsTest : SysuiTestCase() { testScope.runTest { val flow1 = MutableStateFlow(false) val flow2 = MutableStateFlow(false) - val values by collectValues(or(flow1, flow2)) + val values by collectValues(anyOf(flow1, flow2)) assertThat(values).containsExactly(false) flow1.value = true diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index c43e394cb97a..da12dd731c23 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -36,7 +36,6 @@ </style> <style name="Keyguard.Bouncer.SecondaryMessage" parent="Theme.SystemUI"> <item name="android:textSize">14sp</item> - <item name="android:lineHeight">20dp</item> <item name="android:maxLines">@integer/bouncer_secondary_message_lines</item> <item name="android:lines">@integer/bouncer_secondary_message_lines</item> <item name="android:textAlignment">center</item> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 45bcd829336f..b1ba0882d9ba 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1158,6 +1158,8 @@ <string name="button_to_configure_widgets_text">Customize widgets</string> <!-- Description for the App icon of disabled widget. [CHAR LIMIT=NONE] --> <string name="icon_description_for_disabled_widget">App icon for disabled widget</string> + <!-- Description for the App icon of a package that is currently being installed. [CHAR LIMIT=NONE] --> + <string name="icon_description_for_pending_widget">App icon for a widget being installed</string> <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] --> <string name="edit_widget">Edit widget</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 91fb6888bf06..905a98c2e181 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -35,6 +35,7 @@ import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -134,6 +135,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final DevicePolicyManager mDevicePolicyManager; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise // the audio service will bring up the volume dialog. @@ -460,6 +462,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard SelectedUserInteractor selectedUserInteractor, DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, + DevicePolicyManager devicePolicyManager, KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor, Provider<DeviceEntryInteractor> deviceEntryInteractor @@ -495,6 +498,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mKeyguardTransitionInteractor = keyguardTransitionInteractor; mDeviceProvisionedController = deviceProvisionedController; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mDevicePolicyManager = devicePolicyManager; } @Override @@ -1105,35 +1109,23 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); - final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); final int failedAttemptsBeforeWipe = - dpm.getMaximumFailedPasswordsForWipe(null, userId); + mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId); final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? (failedAttemptsBeforeWipe - failedAttempts) : Integer.MAX_VALUE; // because DPM returns 0 if no restriction if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { - // The user has installed a DevicePolicyManager that requests a user/profile to be wiped - // N attempts. Once we get below the grace period, we post this dialog every time as a - // clear warning until the deletion fires. - // Check which profile has the strictest policy for failed password attempts - final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId); - int userType = USER_TYPE_PRIMARY; - if (expiringUser == userId) { - // TODO: http://b/23522538 - if (expiringUser != UserHandle.USER_SYSTEM) { - userType = USER_TYPE_SECONDARY_USER; - } - } else if (expiringUser != UserHandle.USER_NULL) { - userType = USER_TYPE_WORK_PROFILE; - } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY - if (remainingBeforeWipe > 0) { - mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); - } else { - // Too many attempts. The device will be wiped shortly. - Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!"); - mView.showWipeDialog(failedAttempts, userType); - } + // The user has installed a DevicePolicyManager that requests a + // user/profile to be wiped N attempts. Once we get below the grace period, + // we post this dialog every time as a clear warning until the deletion + // fires. Check which profile has the strictest policy for failed password + // attempts. + final int expiringUser = + mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId); + Integer mainUser = mSelectedUserInteractor.getMainUserId(); + showMessageForFailedUnlockAttempt( + userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts); } mLockPatternUtils.reportFailedPasswordAttempt(userId); if (timeoutMs > 0) { @@ -1145,6 +1137,35 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } + @VisibleForTesting + void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId, + int remainingBeforeWipe, int failedAttempts) { + int userType = USER_TYPE_PRIMARY; + if (expiringUserId == userId) { + int primaryUser = UserHandle.USER_SYSTEM; + if (Flags.headlessSingleUserFixes()) { + if (mainUserId != null) { + primaryUser = mainUserId; + } + } + // TODO: http://b/23522538 + if (expiringUserId != primaryUser) { + userType = USER_TYPE_SECONDARY_USER; + } + } else if (expiringUserId != UserHandle.USER_NULL) { + userType = USER_TYPE_WORK_PROFILE; + } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY + if (remainingBeforeWipe > 0) { + mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, + userType); + } else { + // Too many attempts. The device will be wiped shortly. + Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId + + " will be wiped!"); + mView.showWipeDialog(failedAttempts, userType); + } + } + private void getCurrentSecurityController( KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) { mSecurityViewFlipperController diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 5df7fc9865ff..fcba425f0956 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.flags.Flags import android.os.UserHandle import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView @@ -288,9 +289,15 @@ constructor( private suspend fun getWipeTarget(): WipeTarget { // Check which profile has the strictest policy for failed authentication attempts. val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe() + val primaryUser = + if (Flags.headlessSingleUserFixes()) { + selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM + } else { + UserHandle.USER_SYSTEM + } return when (userToBeWiped) { selectedUserInteractor.getSelectedUserId() -> - if (userToBeWiped == UserHandle.USER_SYSTEM) { + if (userToBeWiped == primaryUser) { WipeTarget.WholeDevice } else { WipeTarget.User diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.kt new file mode 100644 index 000000000000..2e9169e03d80 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogModule.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.bluetooth.qsdialog + +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module + +@Module +interface BluetoothTileDialogModule { + @Binds + @SysUISingleton + fun bindDeviceItemActionInteractor( + impl: DeviceItemActionInteractorImpl + ): DeviceItemActionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index 4369f3f64613..94f465d3c1c3 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -62,6 +62,7 @@ internal class BluetoothTileDialogViewModel @Inject constructor( private val deviceItemInteractor: DeviceItemInteractor, + private val deviceItemActionInteractor: DeviceItemActionInteractor, private val bluetoothStateInteractor: BluetoothStateInteractor, private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, private val audioSharingInteractor: AudioSharingInteractor, @@ -192,7 +193,7 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick - .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) } + .onEach { deviceItemActionInteractor.onClick(it, dialog) } .launchIn(this) // contentHeight is emitted when the dialog is dismissed. diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt new file mode 100644 index 000000000000..931176003b1b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -0,0 +1,72 @@ +/* + * 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.bluetooth.qsdialog + +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +/** Defines interface for click handling of a DeviceItem. */ +interface DeviceItemActionInteractor { + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) +} + +@SysUISingleton +open class DeviceItemActionInteractorImpl +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val logger: BluetoothTileDialogLogger, + private val uiEventLogger: UiEventLogger, +) : DeviceItemActionInteractor { + + override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { + withContext(backgroundDispatcher) { + logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type) + + deviceItem.cachedBluetoothDevice.apply { + when (deviceItem.type) { + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> { + disconnect() + uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT) + } + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) + } + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { + setActive() + uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) + } + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> { + disconnect() + uiEventLogger.log( + BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT + ) + } + DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { + connect() + uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt index 66e593b94b21..1526cd9675c7 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt @@ -20,7 +20,6 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context import android.media.AudioManager -import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager @@ -52,7 +51,6 @@ constructor( private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(), private val localBluetoothManager: LocalBluetoothManager?, private val systemClock: SystemClock, - private val uiEventLogger: UiEventLogger, private val logger: BluetoothTileDialogLogger, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -169,38 +167,6 @@ constructor( ) } - internal suspend fun updateDeviceItemOnClick(deviceItem: DeviceItem) { - withContext(backgroundDispatcher) { - logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type) - - deviceItem.cachedBluetoothDevice.apply { - when (deviceItem.type) { - DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> { - disconnect() - uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT) - } - DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { - uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) - } - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { - setActive() - uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) - } - DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> { - disconnect() - uiEventLogger.log( - BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT - ) - } - DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { - connect() - uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT) - } - } - } - } - } - internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) { deviceItemFactoryList = list } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt index fa19bf478453..e0334a060ee2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt @@ -29,7 +29,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.time.SystemClock import dagger.Lazy import javax.inject.Inject @@ -78,7 +78,7 @@ constructor( bouncerRepository.alternateBouncerUIAvailable } private val isDozingOrAod: Flow<Boolean> = - or( + anyOf( keyguardTransitionInteractor.get().transitionValue(KeyguardState.DOZING).map { it > 0f }, diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java index beaa170943fd..b42a903878a7 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java @@ -206,7 +206,7 @@ class FalsingCollectorImpl implements FalsingCollector { ); final CommunalInteractor communalInteractor = mCommunalInteractorLazy.get(); mJavaAdapter.alwaysCollectFlow( - BooleanFlowOperators.INSTANCE.and( + BooleanFlowOperators.INSTANCE.allOf( communalInteractor.isCommunalEnabled(), communalInteractor.isCommunalShowing()), this::onShowingCommunalHubChanged @@ -292,6 +292,7 @@ class FalsingCollectorImpl implements FalsingCollector { @Override public void onKeyEvent(KeyEvent ev) { + logDebug("REAL: onKeyEvent(" + KeyEvent.actionToString(ev.getAction()) + ")"); // Only collect if it is an ACTION_UP action and is allow-listed if (ev.getAction() == KeyEvent.ACTION_UP && mAcceptedKeycodes.contains(ev.getKeyCode())) { mFalsingDataProvider.onKeyEvent(ev); @@ -300,7 +301,7 @@ class FalsingCollectorImpl implements FalsingCollector { @Override public void onTouchEvent(MotionEvent ev) { - logDebug("REAL: onTouchEvent(" + ev.getActionMasked() + ")"); + logDebug("REAL: onTouchEvent(" + MotionEvent.actionToString(ev.getActionMasked()) + ")"); if (!mKeyguardStateController.isShowing()) { avoidGesture(); return; diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt index b289fa49d06c..6b22137e455e 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorNoOp.kt @@ -61,11 +61,11 @@ class FalsingCollectorNoOp @Inject constructor() : FalsingCollector { } override fun onKeyEvent(ev: KeyEvent) { - logDebug("NOOP: onKeyEvent(${ev.action}") + logDebug("NOOP: onKeyEvent(${KeyEvent.actionToString(ev.action)}") } override fun onTouchEvent(ev: MotionEvent) { - logDebug("NOOP: onTouchEvent(${ev.actionMasked})") + logDebug("NOOP: onTouchEvent(${MotionEvent.actionToString(ev.actionMasked)})") } override fun onMotionEventComplete() { diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt index 5c64dc645283..1c1642905d7d 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.data.repository import android.os.UserHandle import com.android.systemui.common.shared.model.PackageChangeModel +import com.android.systemui.common.shared.model.PackageInstallSession import kotlinx.coroutines.flow.Flow interface PackageChangeRepository { @@ -28,4 +29,7 @@ interface PackageChangeRepository { * [UserHandle.USER_ALL] may be used to listen to all users. */ fun packageChanged(user: UserHandle): Flow<PackageChangeModel> + + /** Emits a list of all known install sessions associated with the primary user. */ + val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> } diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt index 712a3527b7c9..41b03f1f3de6 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt @@ -18,6 +18,7 @@ package com.android.systemui.common.data.repository import android.os.UserHandle import com.android.systemui.common.shared.model.PackageChangeModel +import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.filter class PackageChangeRepositoryImpl @Inject constructor( + packageInstallerMonitor: PackageInstallerMonitor, private val monitorFactory: PackageUpdateMonitor.Factory, ) : PackageChangeRepository { /** @@ -37,4 +39,7 @@ constructor( override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> = monitor.packageChanged.filter { user == UserHandle.ALL || user == it.user } + + override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> = + packageInstallerMonitor.installSessionsForPrimaryUser } diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt new file mode 100644 index 000000000000..46db34618c70 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt @@ -0,0 +1,154 @@ +/* + * 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.common.data.repository + +import android.content.pm.PackageInstaller +import android.os.Handler +import com.android.internal.annotations.GuardedBy +import com.android.systemui.common.shared.model.PackageInstallSession +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.PackageChangeRepoLog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +/** Monitors package install sessions for all users. */ +@SysUISingleton +class PackageInstallerMonitor +@Inject +constructor( + @Background private val bgHandler: Handler, + @Background private val bgScope: CoroutineScope, + @PackageChangeRepoLog logBuffer: LogBuffer, + private val packageInstaller: PackageInstaller, +) : PackageInstaller.SessionCallback() { + + private val logger = Logger(logBuffer, TAG) + + @GuardedBy("sessions") private val sessions = mutableMapOf<Int, PackageInstallSession>() + + private val _installSessions = + MutableStateFlow<List<PackageInstallSession>>(emptyList()).apply { + subscriptionCount + .map { count -> count > 0 } + .distinctUntilChanged() + // Drop initial false value + .dropWhile { !it } + .onEach { isActive -> + if (isActive) { + synchronized(sessions) { + sessions.putAll( + packageInstaller.allSessions + .map { session -> session.toModel() } + .associateBy { it.sessionId } + ) + updateInstallerSessionsFlow() + } + packageInstaller.registerSessionCallback( + this@PackageInstallerMonitor, + bgHandler + ) + } else { + synchronized(sessions) { + sessions.clear() + updateInstallerSessionsFlow() + } + packageInstaller.unregisterSessionCallback(this@PackageInstallerMonitor) + } + } + .launchIn(bgScope) + } + + val installSessionsForPrimaryUser: Flow<List<PackageInstallSession>> = + _installSessions.asStateFlow() + + /** Called when a new installer session is created. */ + override fun onCreated(sessionId: Int) { + logger.i({ "session created $int1" }) { int1 = sessionId } + updateSession(sessionId) + } + + /** Called when new installer session has finished. */ + override fun onFinished(sessionId: Int, success: Boolean) { + logger.i({ "session finished $int1" }) { int1 = sessionId } + synchronized(sessions) { + sessions.remove(sessionId) + updateInstallerSessionsFlow() + } + } + + /** + * Badging details for the session changed. For example, the app icon or label has been updated. + */ + override fun onBadgingChanged(sessionId: Int) { + logger.i({ "session badging changed $int1" }) { int1 = sessionId } + updateSession(sessionId) + } + + /** + * A session is considered active when there is ongoing forward progress being made. For + * example, a package started downloading. + */ + override fun onActiveChanged(sessionId: Int, active: Boolean) { + // Active status updates are not tracked for now + } + + override fun onProgressChanged(sessionId: Int, progress: Float) { + // Progress updates are not tracked for now + } + + private fun updateSession(sessionId: Int) { + val session = packageInstaller.getSessionInfo(sessionId) + + synchronized(sessions) { + if (session == null) { + sessions.remove(sessionId) + } else { + sessions[sessionId] = session.toModel() + } + updateInstallerSessionsFlow() + } + } + + @GuardedBy("sessions") + private fun updateInstallerSessionsFlow() { + _installSessions.value = sessions.values.toList() + } + + companion object { + const val TAG = "PackageInstallerMonitor" + + private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession { + return PackageInstallSession( + sessionId = this.sessionId, + packageName = this.appPackageName, + icon = this.getAppIcon(), + user = this.user, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt new file mode 100644 index 000000000000..7025229743b9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt @@ -0,0 +1,28 @@ +/* + * 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.common.shared.model + +import android.graphics.Bitmap +import android.os.UserHandle + +/** Represents a session of a package being installed on device. */ +data class PackageInstallSession( + val sessionId: Int, + val packageName: String, + val icon: Bitmap?, + val user: UserHandle, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 1f54e70fa21b..fdb797d5ba06 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -17,14 +17,13 @@ package com.android.systemui.communal.data.repository import android.app.backup.BackupManager -import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.os.UserHandle -import androidx.annotation.WorkerThread +import com.android.systemui.common.data.repository.PackageChangeRepository +import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.communal.data.backup.CommunalBackupUtils -import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao -import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.nano.CommunalHubState import com.android.systemui.communal.proto.toCommunalHubState import com.android.systemui.communal.shared.model.CommunalWidgetContentModel @@ -36,13 +35,15 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.util.kotlin.getValue -import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -88,7 +89,6 @@ interface CommunalWidgetRepository { class CommunalWidgetRepositoryImpl @Inject constructor( - appWidgetManagerOptional: Optional<AppWidgetManager>, private val appWidgetHost: CommunalAppWidgetHost, @Background private val bgScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, @@ -97,6 +97,7 @@ constructor( @CommunalLog logBuffer: LogBuffer, private val backupManager: BackupManager, private val backupUtils: CommunalBackupUtils, + packageChangeRepository: PackageChangeRepository, ) : CommunalWidgetRepository { companion object { const val TAG = "CommunalWidgetRepository" @@ -104,12 +105,39 @@ constructor( private val logger = Logger(logBuffer, TAG) - private val appWidgetManager by appWidgetManagerOptional + /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */ + private val widgetEntries: Flow<List<CommunalWidgetEntry>> = + combine( + communalWidgetDao.getWidgets(), + communalWidgetHost.appWidgetProviders, + ) { entries, providers -> + entries.mapNotNull { (rank, widget) -> + CommunalWidgetEntry( + appWidgetId = widget.widgetId, + componentName = widget.componentName, + priority = rank.rank, + providerInfo = providers[widget.widgetId] + ) + } + } + @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = - communalWidgetDao - .getWidgets() - .map { it.mapNotNull(::mapToContentModel) } + widgetEntries + .flatMapLatest { widgetEntries -> + // If and only if any widget is missing provider info, combine with the package + // installer sessions flow to check whether they are pending installation. This can + // happen after widgets are freshly restored from a backup. In most cases, provider + // info is available to all widgets, and is unnecessary to involve an API call to + // the package installer. + if (widgetEntries.any { it.providerInfo == null }) { + packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions -> + widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) } + } + } else { + flowOf(widgetEntries.map(::mapToContentModel)) + } + } // As this reads from a database and triggers IPCs to AppWidgetManager, // it should be executed in the background. .flowOn(bgDispatcher) @@ -245,6 +273,9 @@ constructor( } appWidgetHost.deleteAppWidgetId(widgetId) } + + // Providers may have changed + communalWidgetHost.refreshProviders() } } @@ -255,16 +286,57 @@ constructor( } } - @WorkerThread + /** + * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the + * [AppWidgetProviderInfo] of the entry is available. + */ + private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel { + return CommunalWidgetContentModel.Available( + appWidgetId = entry.appWidgetId, + providerInfo = entry.providerInfo!!, + priority = entry.priority, + ) + } + + /** + * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install + * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the + * install sessions, the entry is mapped to a pending widget. + */ private fun mapToContentModel( - entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> + entry: CommunalWidgetEntry, + installSessions: List<PackageInstallSession>, ): CommunalWidgetContentModel? { - val (_, widgetId) = entry.value - val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null - return CommunalWidgetContentModel( - appWidgetId = widgetId, - providerInfo = providerInfo, - priority = entry.key.rank, - ) + if (entry.providerInfo != null) { + return CommunalWidgetContentModel.Available( + appWidgetId = entry.appWidgetId, + providerInfo = entry.providerInfo!!, + priority = entry.priority, + ) + } + + val session = + installSessions.firstOrNull { + it.packageName == + ComponentName.unflattenFromString(entry.componentName)?.packageName + } + return if (session != null) { + CommunalWidgetContentModel.Pending( + appWidgetId = entry.appWidgetId, + priority = entry.priority, + packageName = session.packageName, + icon = session.icon, + user = session.user, + ) + } else { + null + } } + + private data class CommunalWidgetEntry( + val appWidgetId: Int, + val componentName: String, + val priority: Int, + var providerInfo: AppWidgetProviderInfo? = null, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 0042915d0cd9..6b4cf79d59b1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -60,9 +60,9 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.UserTracker import com.android.systemui.smartspace.data.repository.SmartspaceRepository -import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not -import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.emitOnStart import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -127,10 +127,10 @@ constructor( /** Whether communal features are enabled and available. */ val isCommunalAvailable: Flow<Boolean> = - and( + allOf( communalSettingsInteractor.isCommunalEnabled, not(keyguardInteractor.isEncryptedOrLockdown), - or(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming) + anyOf(keyguardInteractor.isKeyguardShowing, keyguardInteractor.isDreaming) ) .distinctUntilChanged() .onEach { available -> @@ -403,19 +403,30 @@ constructor( updateOnWorkProfileBroadcastReceived, ) { widgets, allowedCategories, _ -> widgets.map { widget -> - if (widget.providerInfo.widgetCategory and allowedCategories != 0) { - // At least one category this widget specified is allowed, so show it - WidgetContent.Widget( - appWidgetId = widget.appWidgetId, - providerInfo = widget.providerInfo, - appWidgetHost = appWidgetHost, - inQuietMode = isQuietModeEnabled(widget.providerInfo.profile) - ) - } else { - WidgetContent.DisabledWidget( - appWidgetId = widget.appWidgetId, - providerInfo = widget.providerInfo, - ) + when (widget) { + is CommunalWidgetContentModel.Available -> { + if (widget.providerInfo.widgetCategory and allowedCategories != 0) { + // At least one category this widget specified is allowed, so show it + WidgetContent.Widget( + appWidgetId = widget.appWidgetId, + providerInfo = widget.providerInfo, + appWidgetHost = appWidgetHost, + inQuietMode = isQuietModeEnabled(widget.providerInfo.profile) + ) + } else { + WidgetContent.DisabledWidget( + appWidgetId = widget.appWidgetId, + providerInfo = widget.providerInfo, + ) + } + } + is CommunalWidgetContentModel.Pending -> { + WidgetContent.PendingWidget( + appWidgetId = widget.appWidgetId, + packageName = widget.packageName, + icon = widget.icon, + ) + } } } } @@ -430,7 +441,15 @@ constructor( } else { // Get associated work profile for the currently selected user. val workProfile = userTracker.userProfiles.find { it.isManagedProfile } - list.filter { it.providerInfo.profile.identifier != workProfile?.id } + list.filter { model -> + val uid = + when (model) { + is CommunalWidgetContentModel.Available -> + model.providerInfo.profile.identifier + is CommunalWidgetContentModel.Pending -> model.user.identifier + } + uid != workProfile?.id + } } /** A flow of available smartspace targets. Currently only showing timers. */ @@ -513,7 +532,11 @@ constructor( ): List<CommunalWidgetContentModel> { val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() return list.filter { widget -> - currentUserIds.contains(widget.providerInfo.profile?.identifier) + when (widget) { + is CommunalWidgetContentModel.Available -> + currentUserIds.contains(widget.providerInfo.profile?.identifier) + is CommunalWidgetContentModel.Pending -> true + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 706122789563..122240daed52 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal.domain.model import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.pm.ApplicationInfo +import android.graphics.Bitmap import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.widgets.CommunalAppWidgetHost @@ -45,11 +46,10 @@ sealed interface CommunalContentModel { sealed interface WidgetContent : CommunalContentModel { val appWidgetId: Int - val providerInfo: AppWidgetProviderInfo data class Widget( override val appWidgetId: Int, - override val providerInfo: AppWidgetProviderInfo, + val providerInfo: AppWidgetProviderInfo, val appWidgetHost: CommunalAppWidgetHost, val inQuietMode: Boolean, ) : WidgetContent { @@ -66,7 +66,7 @@ sealed interface CommunalContentModel { data class DisabledWidget( override val appWidgetId: Int, - override val providerInfo: AppWidgetProviderInfo + val providerInfo: AppWidgetProviderInfo ) : WidgetContent { override val key = KEY.disabledWidget(appWidgetId) // Widget size is always half. @@ -75,6 +75,16 @@ sealed interface CommunalContentModel { val appInfo: ApplicationInfo? get() = providerInfo.providerInfo?.applicationInfo } + + data class PendingWidget( + override val appWidgetId: Int, + val packageName: String, + val icon: Bitmap? = null, + ) : WidgetContent { + override val key = KEY.pendingWidget(appWidgetId) + // Widget size is always half. + override val size = CommunalContentSize.HALF + } } /** A placeholder item representing a new widget being added */ @@ -127,6 +137,10 @@ sealed interface CommunalContentModel { return "disabled_widget_$id" } + fun pendingWidget(id: Int): String { + return "pending_widget_$id" + } + fun widgetPlaceholder(): String { return "widget_placeholder_${UUID.randomUUID()}" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt index e141dc40477c..53aecc199c4b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -17,10 +17,27 @@ package com.android.systemui.communal.shared.model import android.appwidget.AppWidgetProviderInfo +import android.graphics.Bitmap +import android.os.UserHandle /** Encapsulates data for a communal widget. */ -data class CommunalWidgetContentModel( - val appWidgetId: Int, - val providerInfo: AppWidgetProviderInfo, - val priority: Int, -) +sealed interface CommunalWidgetContentModel { + val appWidgetId: Int + val priority: Int + + /** Widget is ready to display */ + data class Available( + override val appWidgetId: Int, + val providerInfo: AppWidgetProviderInfo, + override val priority: Int, + ) : CommunalWidgetContentModel + + /** Widget is pending installation */ + data class Pending( + override val appWidgetId: Int, + override val priority: Int, + val packageName: String, + val icon: Bitmap?, + val user: UserHandle, + ) : CommunalWidgetContentModel +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 3f92223fb57b..f6122ad48300 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -104,7 +104,12 @@ constructor( ): Boolean = withContext(backgroundDispatcher) { val widgets = communalInteractor.widgetContent.first() - val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo } + val excludeList = + widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo( + ArrayList() + ) { + it.providerInfo + } getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let { try { activityLauncher.launch(it) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt index 5f1d89e079a7..b7e8205e6582 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt @@ -24,6 +24,7 @@ import android.os.Looper import android.widget.RemoteViews import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger +import javax.annotation.concurrent.GuardedBy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow @@ -47,6 +48,8 @@ class CommunalAppWidgetHost( /** App widget ids that have been removed and no longer available. */ val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow() + @GuardedBy("observers") private val observers = mutableSetOf<Observer>() + override fun onCreateView( context: Context, appWidgetId: Int, @@ -77,6 +80,61 @@ class CommunalAppWidgetHost( } } + override fun allocateAppWidgetId(): Int { + return super.allocateAppWidgetId().also { appWidgetId -> + backgroundScope.launch { + observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) } + } + } + } + + override fun deleteAppWidgetId(appWidgetId: Int) { + super.deleteAppWidgetId(appWidgetId) + backgroundScope.launch { + observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) } + } + } + + override fun startListening() { + super.startListening() + backgroundScope.launch { observers.forEach { observer -> observer.onHostStartListening() } } + } + + override fun stopListening() { + super.stopListening() + backgroundScope.launch { observers.forEach { observer -> observer.onHostStopListening() } } + } + + fun addObserver(observer: Observer) { + synchronized(observers) { observers.add(observer) } + } + + fun removeObserver(observer: Observer) { + synchronized(observers) { observers.remove(observer) } + } + + /** + * Allows another class to observe the [CommunalAppWidgetHost] and handle any logic there. + * + * This is mainly for testability as it is difficult to test a real instance of [AppWidgetHost] + * which communicates with framework services. + * + * Note: all the callbacks are launched from the background scope. + */ + interface Observer { + /** Called immediately after the host has started listening for widget updates. */ + fun onHostStartListening() {} + + /** Called immediately after the host has stopped listening for widget updates. */ + fun onHostStopListening() {} + + /** Called immediately after a new app widget id has been allocated. */ + fun onAllocateAppWidgetId(appWidgetId: Int) {} + + /** Called immediately after an app widget id is to be deleted. */ + fun onDeleteAppWidgetId(appWidgetId: Int) {} + } + companion object { private const val TAG = "CommunalAppWidgetHost" } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index 8390d62b23db..301da51c8082 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.UserTracker -import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -39,6 +39,7 @@ class CommunalAppWidgetHostStartable @Inject constructor( private val appWidgetHost: CommunalAppWidgetHost, + private val communalWidgetHost: CommunalWidgetHost, private val communalInteractor: CommunalInteractor, private val userTracker: UserTracker, @Background private val bgScope: CoroutineScope, @@ -46,7 +47,7 @@ constructor( ) : CoreStartable { override fun start() { - or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) // Only trigger updates on state changes, ignoring the initial false value. .pairwise(false) .filter { (previous, new) -> previous != new } @@ -70,9 +71,11 @@ constructor( // Always ensure this is called on the main/ui thread. withContext(uiDispatcher) { if (active) { + communalWidgetHost.startObservingHost() appWidgetHost.startListening() } else { appWidgetHost.stopListening() + communalWidgetHost.stopObservingHost() } } @@ -83,7 +86,15 @@ constructor( private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) { val currentUserIds = userTracker.userProfiles.map { it.id }.toSet() widgets - .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) } + .filter { widget -> + val uid = + when (widget) { + is CommunalWidgetContentModel.Available -> + widget.providerInfo.profile?.identifier + is CommunalWidgetContentModel.Pending -> widget.user.identifier + } + !currentUserIds.contains(uid) + } .onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt index 93e2b37cfe87..42107c1e9769 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.widgets +import android.appwidget.AppWidgetHost.AppWidgetHostListener import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL @@ -23,6 +24,9 @@ import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName import android.os.Bundle import android.os.UserHandle +import android.widget.RemoteViews +import androidx.annotation.WorkerThread +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -30,6 +34,11 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch /** * Widget host that interacts with AppWidget service and host to bind and provide info for widgets @@ -38,11 +47,12 @@ import javax.inject.Inject class CommunalWidgetHost @Inject constructor( + @Background private val bgScope: CoroutineScope, private val appWidgetManager: Optional<AppWidgetManager>, private val appWidgetHost: CommunalAppWidgetHost, private val selectedUserInteractor: SelectedUserInteractor, @CommunalLog logBuffer: LogBuffer, -) { +) : CommunalAppWidgetHost.Observer { companion object { private const val TAG = "CommunalWidgetHost" @@ -60,6 +70,19 @@ constructor( private val logger = Logger(logBuffer, TAG) + private val _appWidgetProviders = MutableStateFlow(emptyMap<Int, AppWidgetProviderInfo?>()) + + /** + * A flow of mappings between an appWidgetId and its corresponding [AppWidgetProviderInfo]. + * These [AppWidgetProviderInfo]s represent app widgets that are actively bound to the + * [CommunalAppWidgetHost]. + * + * The [AppWidgetProviderInfo] may be null in the case that the widget is bound but its provider + * is unavailable. For example, its package is not installed. + */ + val appWidgetProviders: StateFlow<Map<Int, AppWidgetProviderInfo?>> = + _appWidgetProviders.asStateFlow() + /** * Allocate an app widget id and binds the widget with the provider and associated user. * @@ -77,6 +100,7 @@ constructor( ) ) { logger.d("Successfully bound the widget $provider") + onProviderInfoUpdated(id, getAppWidgetInfo(id)) return id } appWidgetHost.deleteAppWidgetId(id) @@ -100,7 +124,83 @@ constructor( return false } + @WorkerThread fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? { return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId) } + + fun startObservingHost() { + appWidgetHost.addObserver(this@CommunalWidgetHost) + } + + fun stopObservingHost() { + appWidgetHost.removeObserver(this@CommunalWidgetHost) + } + + fun refreshProviders() { + bgScope.launch { + val newProviders = mutableMapOf<Int, AppWidgetProviderInfo?>() + appWidgetHost.appWidgetIds.forEach { appWidgetId -> + // Listen for updates from each bound widget + addListener(appWidgetId) + + // Fetch provider info of the widget + newProviders[appWidgetId] = getAppWidgetInfo(appWidgetId) + } + + _appWidgetProviders.value = newProviders.toMap() + } + } + + override fun onHostStartListening() { + refreshProviders() + } + + override fun onHostStopListening() { + // Remove listeners + _appWidgetProviders.value.keys.forEach { appWidgetId -> + appWidgetHost.removeListener(appWidgetId) + } + + // Clear providers + _appWidgetProviders.value = emptyMap() + } + + override fun onAllocateAppWidgetId(appWidgetId: Int) { + addListener(appWidgetId) + } + + override fun onDeleteAppWidgetId(appWidgetId: Int) { + appWidgetHost.removeListener(appWidgetId) + _appWidgetProviders.value = + _appWidgetProviders.value.toMutableMap().also { it.remove(appWidgetId) } + } + + private fun addListener(appWidgetId: Int) { + appWidgetHost.setListener( + appWidgetId, + CommunalAppWidgetHostListener(appWidgetId, this::onProviderInfoUpdated), + ) + } + + private fun onProviderInfoUpdated(appWidgetId: Int, providerInfo: AppWidgetProviderInfo?) { + bgScope.launch { + _appWidgetProviders.value = + _appWidgetProviders.value.toMutableMap().also { it[appWidgetId] = providerInfo } + } + } + + /** A [AppWidgetHostListener] for [appWidgetId]. */ + private class CommunalAppWidgetHostListener( + private val appWidgetId: Int, + private val onUpdateProviderInfo: (Int, AppWidgetProviderInfo?) -> Unit, + ) : AppWidgetHostListener { + override fun onUpdateProviderInfo(providerInfo: AppWidgetProviderInfo?) { + onUpdateProviderInfo(appWidgetId, providerInfo) + } + + override fun onViewDataChanged(viewId: Int) {} + + override fun updateAppWidget(remoteViews: RemoteViews?) {} + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt index aa6516d54563..2000f96bcdb0 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt @@ -69,16 +69,18 @@ interface CommunalWidgetModule { @SysUISingleton @Provides fun provideCommunalWidgetHost( + @Application applicationScope: CoroutineScope, appWidgetManager: Optional<AppWidgetManager>, appWidgetHost: CommunalAppWidgetHost, selectedUserInteractor: SelectedUserInteractor, @CommunalLog logBuffer: LogBuffer, ): CommunalWidgetHost { return CommunalWidgetHost( + applicationScope, appWidgetManager, appWidgetHost, selectedUserInteractor, - logBuffer + logBuffer, ) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index ef3f10fa3853..e00137e3045e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -49,6 +49,7 @@ import android.content.SharedPreferences; import android.content.om.OverlayManager; import android.content.pm.IPackageManager; import android.content.pm.LauncherApps; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.AssetManager; @@ -483,6 +484,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static PackageInstaller providePackageInstaller(PackageManager packageManager) { + return packageManager.getPackageInstaller(); + } + + @Provides + @Singleton static PackageManagerWrapper providePackageManagerWrapper() { return PackageManagerWrapper.getInstance(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt index c6fb4f9d6956..fc9406bd27d8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt @@ -19,12 +19,13 @@ package com.android.systemui.keyboard import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl +import com.android.systemui.keyboard.shortcut.ShortcutHelperModule import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl import dagger.Binds import dagger.Module -@Module +@Module(includes = [ShortcutHelperModule::class]) abstract class KeyboardModule { @Binds diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt new file mode 100644 index 000000000000..5635f8056b9c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt @@ -0,0 +1,69 @@ +/* + * 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.keyboard.shortcut + +import android.app.Activity +import com.android.systemui.CoreStartable +import com.android.systemui.Flags.keyboardShortcutHelperRewrite +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository +import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter +import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity +import dagger.Binds +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface ShortcutHelperModule { + + @Binds + @IntoMap + @ClassKey(ShortcutHelperActivity::class) + fun activity(impl: ShortcutHelperActivity): Activity + + companion object { + @Provides + @IntoMap + @ClassKey(ShortcutHelperActivityStarter::class) + fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable { + return if (keyboardShortcutHelperRewrite()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + NoOpStartable + } + } + + @Provides + @IntoMap + @ClassKey(ShortcutHelperRepository::class) + fun repo(implLazy: Lazy<ShortcutHelperRepository>): CoreStartable { + return if (keyboardShortcutHelperRewrite()) { + implLazy.get() + } else { + // No-op implementation when the flag is disabled. + NoOpStartable + } + } + } +} + +private object NoOpStartable : CoreStartable { + override fun start() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt new file mode 100644 index 000000000000..9450af4c804e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.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.systemui.keyboard.shortcut.data.repository + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.android.systemui.CoreStartable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive +import com.android.systemui.statusbar.CommandQueue +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +@SysUISingleton +class ShortcutHelperRepository +@Inject +constructor( + private val commandQueue: CommandQueue, + private val broadcastDispatcher: BroadcastDispatcher, +) : CoreStartable { + + val state = MutableStateFlow<ShortcutHelperState>(Inactive) + + override fun start() { + registerBroadcastReceiver( + action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS, + onReceive = { state.value = Active() } + ) + registerBroadcastReceiver( + action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS, + onReceive = { state.value = Inactive } + ) + commandQueue.addCallback( + object : CommandQueue.Callbacks { + override fun dismissKeyboardShortcutsMenu() { + state.value = Inactive + } + + override fun toggleKeyboardShortcutsMenu(deviceId: Int) { + state.value = + if (state.value is Inactive) { + Active(deviceId) + } else { + Inactive + } + } + } + ) + } + + fun hide() { + state.value = Inactive + } + + private fun registerBroadcastReceiver(action: String, onReceive: () -> Unit) { + broadcastDispatcher.registerReceiver( + receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + onReceive() + } + }, + filter = IntentFilter(action), + flags = Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt new file mode 100644 index 000000000000..d3f7e24bb87f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class ShortcutHelperInteractor +@Inject +constructor(private val repository: ShortcutHelperRepository) { + + val state: Flow<ShortcutHelperState> = repository.state + + fun onUserLeave() { + repository.hide() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt new file mode 100644 index 000000000000..d22d6c88ccc8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt @@ -0,0 +1,23 @@ +/* + * 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.keyboard.shortcut.shared.model + +sealed interface ShortcutHelperState { + data object Inactive : ShortcutHelperState + + data class Active(val deviceId: Int? = null) : ShortcutHelperState +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt new file mode 100644 index 000000000000..fbf52e773599 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt @@ -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.systemui.keyboard.shortcut.ui + +import android.content.Context +import android.content.Intent +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity +import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SysUISingleton +class ShortcutHelperActivityStarter( + private val context: Context, + @Application private val applicationScope: CoroutineScope, + private val viewModel: ShortcutHelperViewModel, + private val startActivity: (Intent) -> Unit, +) : CoreStartable { + + @Inject + constructor( + context: Context, + @Application applicationScope: CoroutineScope, + viewModel: ShortcutHelperViewModel, + ) : this( + context, + applicationScope, + viewModel, + startActivity = { intent -> context.startActivity(intent) } + ) + + override fun start() { + applicationScope.launch { + viewModel.shouldShow.collect { shouldShow -> + if (shouldShow) { + startShortcutHelperActivity() + } + } + } + } + + private fun startShortcutHelperActivity() { + startActivity( + Intent(context, ShortcutHelperActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt index 692fbb06e88c..934f9ee9e90d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.keyboard.shortcut +package com.android.systemui.keyboard.shortcut.ui.view import android.graphics.Insets import android.os.Bundle @@ -24,16 +24,25 @@ import androidx.activity.BackEventCompat import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.core.view.updatePadding +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel import com.android.systemui.res.R import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN +import javax.inject.Inject +import kotlinx.coroutines.launch /** * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for * small and large screen devices. */ -class ShortcutHelperActivity : ComponentActivity() { +class ShortcutHelperActivity +@Inject +constructor( + private val viewModel: ShortcutHelperViewModel, +) : ComponentActivity() { private val bottomSheetContainer get() = requireViewById<View>(R.id.shortcut_helper_sheet_container) @@ -53,6 +62,24 @@ class ShortcutHelperActivity : ComponentActivity() { setUpPredictiveBack() setUpSheetDismissListener() setUpDismissOnTouchOutside() + observeFinishRequired() + } + + override fun onDestroy() { + super.onDestroy() + if (isFinishing) { + viewModel.onUserLeave() + } + } + + private fun observeFinishRequired() { + lifecycleScope.launch { + viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow -> + if (!shouldShow) { + finish() + } + } + } } private fun setupEdgeToEdge() { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt new file mode 100644 index 000000000000..7e48c6523122 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt @@ -0,0 +1,44 @@ +/* + * 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.keyboard.shortcut.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class ShortcutHelperViewModel +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val interactor: ShortcutHelperInteractor +) { + + val shouldShow = + interactor.state + .map { it is ShortcutHelperState.Active } + .distinctUntilChanged() + .flowOn(backgroundDispatcher) + + fun onUserLeave() { + interactor.onUserLeave() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt index a49b3ae7b7e3..c11c49c7a8a0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository import android.os.Handler import android.util.Log +import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.model.KeyguardBlueprint @@ -57,21 +58,7 @@ constructor( TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) } val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!) val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1) - private var targetTransitionConfig: Config? = null - - /** - * Emits the blueprint value to the collectors. - * - * @param blueprintId - * @return whether the transition has succeeded. - */ - fun applyBlueprint(index: Int): Boolean { - ArrayList(blueprintIdMap.values)[index]?.let { - applyBlueprint(it) - return true - } - return false - } + @VisibleForTesting var targetTransitionConfig: Config? = null /** * Emits the blueprint value to the collectors. @@ -81,27 +68,21 @@ constructor( */ fun applyBlueprint(blueprintId: String?): Boolean { val blueprint = blueprintIdMap[blueprintId] - return if (blueprint != null) { - applyBlueprint(blueprint) - true - } else { + if (blueprint == null) { Log.e( TAG, "Could not find blueprint with id: $blueprintId. " + "Perhaps it was not added to KeyguardBlueprintModule?" ) - false + return false } - } - /** Emits the blueprint value to the collectors. */ - fun applyBlueprint(blueprint: KeyguardBlueprint?) { if (blueprint == this.blueprint.value) { - refreshBlueprint() - return + return true } - blueprint?.let { this.blueprint.value = it } + this.blueprint.value = blueprint + return true } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 54d9a78620e5..faab033441c1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -28,7 +28,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -148,7 +148,7 @@ constructor( } } else { scope.launch { - and(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming)) + allOf(keyguardInteractor.isKeyguardOccluded, not(keyguardInteractor.isDreaming)) .filterRelevantKeyguardStateAnd { isOccludedAndNotDreaming -> isOccludedAndNotDreaming } 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 da4f85e0dd2f..857096e1c03b 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 @@ -36,9 +36,12 @@ import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch @SysUISingleton @@ -64,12 +67,7 @@ constructor( /** Current BlueprintId */ val blueprintId = - combine( - configurationInteractor.onAnyConfigurationChange, - fingerprintPropertyInteractor.propertiesInitialized.filter { it }, - clockInteractor.currentClock, - shadeInteractor.shadeMode, - ) { _, _, _, shadeMode -> + shadeInteractor.shadeMode.map { shadeMode -> val useSplitShade = shadeMode == ShadeMode.Split && !ComposeLockscreen.isEnabled when { useSplitShade -> SplitShadeKeyguardBlueprint.ID @@ -77,17 +75,29 @@ constructor( } } + private val refreshEvents: Flow<Unit> = + merge( + configurationInteractor.onAnyConfigurationChange, + fingerprintPropertyInteractor.propertiesInitialized.filter { it }.map { Unit }, + ) + init { applicationScope.launch { blueprintId.collect { transitionToBlueprint(it) } } + applicationScope.launch { refreshEvents.collect { refreshBlueprint() } } } /** - * Transitions to a blueprint. + * Transitions to a blueprint, or refreshes it if already applied. * * @param blueprintId * @return whether the transition has succeeded. */ - fun transitionToBlueprint(blueprintId: String): Boolean { + fun transitionOrRefreshBlueprint(blueprintId: String): Boolean { + if (blueprintId == blueprint.value.id) { + refreshBlueprint() + return true + } + return keyguardBlueprintRepository.applyBlueprint(blueprintId) } @@ -97,7 +107,7 @@ constructor( * @param blueprintId * @return whether the transition has succeeded. */ - fun transitionToBlueprint(blueprintId: Int): Boolean { + fun transitionToBlueprint(blueprintId: String): Boolean { return keyguardBlueprintRepository.applyBlueprint(blueprintId) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index ccc48b5ecb12..bda6438c308f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -36,7 +36,6 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.app.tracing.coroutines.launch import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.Flags.newAodTransition diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt index ce7ec0e22f1c..962cdf10cf86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt @@ -46,15 +46,14 @@ constructor( return } - if ( - arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt()) - ) { - pw.println("Transition succeeded!") - } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) { - pw.println("Transition succeeded!") - } else { - pw.println("Invalid argument! To see available blueprint ids, run:") - pw.println("$ adb shell cmd statusbar blueprint help") + when { + arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.") + keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) -> + pw.println("Transition succeeded!") + else -> { + pw.println("Invalid argument! To see available blueprint ids, run:") + pw.println("$ adb shell cmd statusbar blueprint help") + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 24a7c512a6a9..bbcea56799ea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -42,7 +42,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent @@ -64,7 +64,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @@ -134,7 +133,7 @@ constructor( private val isOnLockscreen: Flow<Boolean> = combine( keyguardTransitionInteractor.isFinishedInState(LOCKSCREEN).onStart { emit(false) }, - or( + anyOf( keyguardTransitionInteractor.isInTransitionToState(LOCKSCREEN), keyguardTransitionInteractor.isInTransitionFromState(LOCKSCREEN), ), diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index b705a0389300..ea89be61d773 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -21,6 +21,7 @@ import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE; import android.content.Context; import android.os.Handler; +import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogModule; import com.android.systemui.dagger.NightDisplayListenerModule; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -60,6 +61,7 @@ import javax.inject.Named; */ @Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class}, includes = { + BluetoothTileDialogModule.class, MediaModule.class, PanelsModule.class, QSExternalModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt new file mode 100644 index 000000000000..28c1fbf2007d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt @@ -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.systemui.qs.panels.data.repository + +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.panels.shared.model.EditTileData +import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.settings.UserTracker +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +@SysUISingleton +class IconAndNameCustomRepository +@Inject +constructor( + private val installedTilesComponentRepository: InstalledTilesComponentRepository, + private val userTracker: UserTracker, + @Background private val backgroundContext: CoroutineContext, +) { + /** + * Returns a list of the icon/labels for all available (installed and enabled) tile services. + * + * No order is guaranteed. + */ + suspend fun getCustomTileData(): List<EditTileData> { + return withContext(backgroundContext) { + val installedTiles = + installedTilesComponentRepository.getInstalledTilesServiceInfos(userTracker.userId) + val packageManager = userTracker.userContext.packageManager + installedTiles + .map { + val tileSpec = TileSpec.create(it.componentName) + val label = it.loadLabel(packageManager) + val icon = it.loadIcon(packageManager) + val appName = it.applicationInfo.loadLabel(packageManager) + if (icon != null) { + EditTileData( + tileSpec, + Icon.Loaded(icon, ContentDescription.Loaded(label.toString())), + Text.Loaded(label.toString()), + Text.Loaded(appName.toString()), + ) + } else { + null + } + } + .filterNotNull() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt new file mode 100644 index 000000000000..ec9d151a26d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt @@ -0,0 +1,41 @@ +/* + * 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.qs.panels.data.repository + +import android.content.res.Resources +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.res.R +import javax.inject.Inject + +@SysUISingleton +class StockTilesRepository +@Inject +constructor( + @Main private val resources: Resources, +) { + /** + * List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec]. + */ + val stockTiles = + resources + .getString(R.string.quick_settings_tiles_stock) + .split(",") + .map(TileSpec::create) + .filterNot { it is TileSpec.Invalid } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt new file mode 100644 index 000000000000..3b29422ccfc3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt @@ -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.systemui.qs.panels.domain.interactor + +import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.panels.data.repository.IconAndNameCustomRepository +import com.android.systemui.qs.panels.data.repository.StockTilesRepository +import com.android.systemui.qs.panels.domain.model.EditTilesModel +import com.android.systemui.qs.panels.shared.model.EditTileData +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider +import javax.inject.Inject + +@SysUISingleton +class EditTilesListInteractor +@Inject +constructor( + private val stockTilesRepository: StockTilesRepository, + private val qsTileConfigProvider: QSTileConfigProvider, + private val iconAndNameCustomRepository: IconAndNameCustomRepository, +) { + /** + * Provides a list of the tiles to edit, with their UI information (icon, labels). + * + * The icons have the label as their content description. + */ + suspend fun getTilesToEdit(): EditTilesModel { + val stockTiles = + stockTilesRepository.stockTiles.map { + if (qsTileConfigProvider.hasConfig(it.spec)) { + val config = qsTileConfigProvider.getConfig(it.spec) + EditTileData( + it, + Icon.Resource( + config.uiConfig.iconRes, + ContentDescription.Resource(config.uiConfig.labelRes) + ), + Text.Resource(config.uiConfig.labelRes), + null, + ) + } else { + EditTileData( + it, + Icon.Resource( + android.R.drawable.star_on, + ContentDescription.Loaded(it.spec) + ), + Text.Loaded(it.spec), + null + ) + } + } + return EditTilesModel(stockTiles, iconAndNameCustomRepository.getCustomTileData()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt new file mode 100644 index 000000000000..b573b9a8770f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt @@ -0,0 +1,24 @@ +/* + * 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.qs.panels.domain.model + +import com.android.systemui.qs.panels.shared.model.EditTileData + +data class EditTilesModel( + val stockTiles: List<EditTileData>, + val customTiles: List<EditTileData>, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt new file mode 100644 index 000000000000..8b70bb9f9e23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt @@ -0,0 +1,38 @@ +/* + * 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.qs.panels.shared.model + +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.pipeline.shared.TileSpec + +data class EditTileData( + val tileSpec: TileSpec, + val icon: Icon, + val label: Text, + val appName: Text?, +) { + init { + check( + (tileSpec is TileSpec.PlatformTileSpec && appName == null) || + (tileSpec is TileSpec.CustomTileSpec && appName != null) + ) { + "tileSpec: $tileSpec - appName: $appName. " + + "appName must be non-null for custom tiles and only for custom tiles." + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt new file mode 100644 index 000000000000..5c17fd1bf94e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt @@ -0,0 +1,48 @@ +/* + * 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.qs.panels.ui.compose + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel + +@Composable +fun EditMode( + viewModel: EditModeViewModel, + modifier: Modifier = Modifier, +) { + val gridLayout by viewModel.gridLayout.collectAsState() + val tiles by viewModel.tiles.collectAsState(emptyList()) + + BackHandler { viewModel.stopEditing() } + + DisposableEffect(Unit) { onDispose { viewModel.stopEditing() } } + + Column(modifier) { + gridLayout.EditTileGrid( + tiles, + Modifier, + viewModel::addTile, + viewModel::removeTile, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt index 68ce5d830570..8806931a888a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt @@ -18,7 +18,9 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.pipeline.shared.TileSpec interface GridLayout { @Composable @@ -26,4 +28,12 @@ interface GridLayout { tiles: List<TileViewModel>, modifier: Modifier, ) + + @Composable + fun EditTileGrid( + tiles: List<EditTileViewModel>, + modifier: Modifier, + onAddTile: (TileSpec, Int) -> Unit, + onRemoveTile: (TileSpec) -> Unit, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt index e2143e090b21..6539cf35b073 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -37,8 +39,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -47,6 +55,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -56,14 +65,28 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.background import com.android.compose.theme.colorAttr import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.common.ui.compose.load import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor +import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes +import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions +import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel +import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.res.R import javax.inject.Inject @@ -75,6 +98,8 @@ import kotlinx.coroutines.flow.mapLatest class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: IconTilesInteractor) : GridLayout { + private object TileType + @Composable override fun TileGrid( tiles: List<TileViewModel>, @@ -88,17 +113,7 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic val iconTilesSpecs by iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet()) - LazyVerticalGrid( - columns = - GridCells.Fixed( - integerResource(R.integer.quick_settings_infinite_grid_num_columns) - ), - verticalArrangement = - Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), - horizontalArrangement = - Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)), - modifier = modifier - ) { + TileLazyGrid(modifier) { items( tiles.size, span = { index -> @@ -131,29 +146,11 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic .mapLatest { it.toUiState() } .collectAsState(initial = tile.currentState.toUiState()) val context = LocalContext.current - val horizontalAlignment = - if (iconOnly) { - Alignment.CenterHorizontally - } else { - Alignment.Start - } Row( - modifier = - modifier - .fillMaxWidth() - .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))) - .clickable { tile.onClick(null) } - .background(colorAttr(state.colors.background)) - .padding( - horizontal = dimensionResource(id = R.dimen.qs_label_container_margin) - ), + modifier = modifier.clickable { tile.onClick(null) }.tileModifier(state.colors), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = - Arrangement.spacedBy( - space = dimensionResource(id = R.dimen.qs_label_container_margin), - alignment = horizontalAlignment - ) + horizontalArrangement = tileHorizontalArrangement(iconOnly) ) { val icon = remember(state.icon) { @@ -165,62 +162,275 @@ class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: Ic } } } - TileIcon(icon, colorAttr(state.colors.icon)) - - if (!iconOnly) { - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight() - ) { - Text( - state.label.toString(), - color = colorAttr(state.colors.label), - modifier = Modifier.basicMarquee(), - ) - if (!TextUtils.isEmpty(state.secondaryLabel)) { - Text( - state.secondaryLabel.toString(), - color = colorAttr(state.colors.secondaryLabel), - modifier = Modifier.basicMarquee(), - ) - } - } - } + TileContent( + label = state.label.toString(), + secondaryLabel = state.secondaryLabel.toString(), + icon = icon, + colors = state.colors, + iconOnly = iconOnly + ) } } - @OptIn(ExperimentalAnimationGraphicsApi::class) @Composable - private fun TileIcon(icon: Icon, color: Color) { - val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) - val context = LocalContext.current - val loadedDrawable = - remember(icon, context) { - when (icon) { - is Icon.Loaded -> icon.drawable - is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res) + override fun EditTileGrid( + tiles: List<EditTileViewModel>, + modifier: Modifier, + onAddTile: (TileSpec, Int) -> Unit, + onRemoveTile: (TileSpec) -> Unit, + ) { + val (currentTiles, otherTiles) = tiles.partition { it.isCurrent } + val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null } + val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState { + onAddTile(it, POSITION_AT_END) + } + val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet()) + val isIconOnly: (TileSpec) -> Boolean = + remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } } + + TileLazyGrid(modifier = modifier) { + // These Text are just placeholders to see the different sections. Not final UI. + item(span = { GridItemSpan(maxLineSpan) }) { + Text("Current tiles", color = Color.White) + } + + editTiles( + currentTiles, + ClickAction.REMOVE, + onRemoveTile, + isIconOnly, + indicatePosition = true, + ) + + item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) } + + editTiles( + otherTilesStock, + ClickAction.ADD, + addTileToEnd, + isIconOnly, + ) + + item(span = { GridItemSpan(maxLineSpan) }) { + Text("Custom tiles to add", color = Color.White) + } + + editTiles( + otherTilesCustom, + ClickAction.ADD, + addTileToEnd, + isIconOnly, + ) + } + } + + private fun LazyGridScope.editTiles( + tiles: List<EditTileViewModel>, + clickAction: ClickAction, + onClick: (TileSpec) -> Unit, + isIconOnly: (TileSpec) -> Boolean, + indicatePosition: Boolean = false, + ) { + items( + count = tiles.size, + key = { tiles[it].tileSpec.spec }, + span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) }, + contentType = { TileType } + ) { + val viewModel = tiles[it] + val canClick = + when (clickAction) { + ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions + ClickAction.REMOVE -> + AvailableEditActions.REMOVE in viewModel.availableEditActions + } + val onClickActionName = + when (clickAction) { + ClickAction.ADD -> + stringResource(id = R.string.accessibility_qs_edit_tile_add_action) + ClickAction.REMOVE -> + stringResource(id = R.string.accessibility_qs_edit_remove_tile_action) + } + val stateDescription = + if (indicatePosition) { + stringResource(id = R.string.accessibility_qs_edit_position, it + 1) + } else { + "" + } + + Box( + modifier = + Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) } + .animateItem() + .semantics { + onClick(onClickActionName) { false } + this.stateDescription = stateDescription + } + ) { + EditTile( + tileViewModel = viewModel, + isIconOnly(viewModel.tileSpec), + modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height)) + ) + if (canClick) { + Badge(clickAction, Modifier.align(Alignment.TopEnd)) } } - if (loadedDrawable !is Animatable) { + } + } + + @Composable + private fun Badge(action: ClickAction, modifier: Modifier = Modifier) { + Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) { Icon( - icon = icon, - tint = color, - modifier = modifier, + imageVector = + when (action) { + ClickAction.ADD -> Icons.Filled.Add + ClickAction.REMOVE -> Icons.Filled.Remove + }, + "", + tint = Color.Black, ) - } else if (icon is Icon.Resource) { - val image = AnimatedImageVector.animatedVectorResource(id = icon.res) - var atEnd by remember(icon.res) { mutableStateOf(false) } - LaunchedEffect(key1 = icon.res) { - delay(350) - atEnd = true + } + } + + @Composable + private fun EditTile( + tileViewModel: EditTileViewModel, + iconOnly: Boolean, + modifier: Modifier = Modifier, + ) { + val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec + val colors = ActiveTileColorAttributes + + Row( + modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = tileHorizontalArrangement(iconOnly) + ) { + TileContent( + label = label, + secondaryLabel = tileViewModel.appName?.load(), + colors = colors, + icon = tileViewModel.icon, + iconOnly = iconOnly, + animateIconToEnd = true, + ) + } + } + + private enum class ClickAction { + ADD, + REMOVE, + } +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Composable +private fun TileIcon( + icon: Icon, + color: Color, + animateToEnd: Boolean = false, +) { + val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size)) + val context = LocalContext.current + val loadedDrawable = + remember(icon, context) { + when (icon) { + is Icon.Loaded -> icon.drawable + is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res) + } + } + if (loadedDrawable !is Animatable) { + Icon( + icon = icon, + tint = color, + modifier = modifier, + ) + } else if (icon is Icon.Resource) { + val image = AnimatedImageVector.animatedVectorResource(id = icon.res) + val painter = + if (animateToEnd) { + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true) + } else { + var atEnd by remember(icon.res) { mutableStateOf(false) } + LaunchedEffect(key1 = icon.res) { + delay(350) + atEnd = true + } + rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) } - val painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd) - Image( - painter = painter, - contentDescription = null, - colorFilter = ColorFilter.tint(color = color), - modifier = modifier + Image( + painter = painter, + contentDescription = null, + colorFilter = ColorFilter.tint(color = color), + modifier = modifier + ) + } +} + +@Composable +private fun TileLazyGrid( + modifier: Modifier = Modifier, + content: LazyGridScope.() -> Unit, +) { + LazyVerticalGrid( + columns = + GridCells.Fixed(integerResource(R.integer.quick_settings_infinite_grid_num_columns)), + verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), + horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)), + modifier = modifier, + content = content, + ) +} + +@Composable +private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier { + return fillMaxWidth() + .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))) + .background(colorAttr(colors.background)) + .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin)) +} + +@Composable +private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal { + val horizontalAlignment = + if (iconOnly) { + Alignment.CenterHorizontally + } else { + Alignment.Start + } + return spacedBy( + space = dimensionResource(id = R.dimen.qs_label_container_margin), + alignment = horizontalAlignment + ) +} + +@Composable +private fun TileContent( + label: String, + secondaryLabel: String?, + icon: Icon, + colors: TileColorAttributes, + iconOnly: Boolean, + animateIconToEnd: Boolean = false, +) { + TileIcon(icon, colorAttr(colors.icon), animateIconToEnd) + + if (!iconOnly) { + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { + Text( + label, + color = colorAttr(colors.label), + modifier = Modifier.basicMarquee(), ) + if (!TextUtils.isEmpty(secondaryLabel)) { + Text( + secondaryLabel ?: "", + color = colorAttr(colors.secondaryLabel), + modifier = Modifier.basicMarquee(), + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt new file mode 100644 index 000000000000..69f50a7986d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt @@ -0,0 +1,154 @@ +/* + * 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.qs.panels.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor +import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor +import com.android.systemui.qs.panels.shared.model.GridLayoutType +import com.android.systemui.qs.panels.ui.compose.GridLayout +import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END +import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor +import com.android.systemui.qs.pipeline.shared.TileSpec +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) +class EditModeViewModel +@Inject +constructor( + private val editTilesListInteractor: EditTilesListInteractor, + private val currentTilesInteractor: CurrentTilesInteractor, + private val minTilesInteractor: MinimumTilesInteractor, + private val defaultGridLayout: InfiniteGridLayout, + @Application private val applicationScope: CoroutineScope, + gridLayoutTypeInteractor: GridLayoutTypeInteractor, + gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>, +) { + private val _isEditing = MutableStateFlow(false) + + /** + * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this + */ + val isEditing = _isEditing.asStateFlow() + private val minimumTiles: Int + get() = minTilesInteractor.minNumberOfTiles + + val gridLayout: StateFlow<GridLayout> = + gridLayoutTypeInteractor.layout + .map { gridLayoutMap[it] ?: defaultGridLayout } + .stateIn( + applicationScope, + SharingStarted.WhileSubscribed(), + defaultGridLayout, + ) + + /** + * Flow of view models for each tile that should be visible in edit mode (or empty flow when not + * editing). + * + * Guarantees of the data: + * * The data for the tiles is fetched once whenever [isEditing] goes from `false` to `true`. + * This prevents icons/labels changing while in edit mode. + * * It tracks the current tiles as they are added/removed/moved by the user. + * * The tiles that are current will be in the same relative order as the user sees them in + * Quick Settings. + * * The tiles that are not current will preserve their relative order even when the current + * tiles change. + */ + val tiles = + isEditing.flatMapLatest { + if (it) { + val editTilesData = editTilesListInteractor.getTilesToEdit() + currentTilesInteractor.currentTiles.map { tiles -> + val currentSpecs = tiles.map { it.spec } + val canRemoveTiles = currentSpecs.size > minimumTiles + val allTiles = editTilesData.stockTiles + editTilesData.customTiles + val allTilesMap = allTiles.associate { it.tileSpec to it } + val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull() + val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs } + + (currentTiles + nonCurrentTiles).map { + val current = it.tileSpec in currentSpecs + val availableActions = buildSet { + if (current) { + add(AvailableEditActions.MOVE) + if (canRemoveTiles) { + add(AvailableEditActions.REMOVE) + } + } else { + add(AvailableEditActions.ADD) + } + } + EditTileViewModel( + it.tileSpec, + it.icon, + it.label, + it.appName, + current, + availableActions + ) + } + } + } else { + emptyFlow() + } + } + + /** @see isEditing */ + fun startEditing() { + _isEditing.value = true + } + + /** @see isEditing */ + fun stopEditing() { + _isEditing.value = false + } + + /** Immediately moves [tileSpec] to [position]. */ + fun moveTile(tileSpec: TileSpec, position: Int) { + throw NotImplementedError("This is not supported yet") + } + + /** Immediately adds [tileSpec] to the current tiles at [position]. */ + fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) { + currentTilesInteractor.addTile(tileSpec, position) + } + + /** Immediately removes [tileSpec] from the current tiles. */ + fun removeTile(tileSpec: TileSpec) { + currentTilesInteractor.removeTiles(listOf(tileSpec)) + } + + /** Immediately resets the current tiles to the default list. */ + fun resetCurrentTilesToDefault() { + throw NotImplementedError("This is not supported yet") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt new file mode 100644 index 000000000000..ba9a0442503d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt @@ -0,0 +1,42 @@ +/* + * 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.qs.panels.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.shared.model.Text +import com.android.systemui.qs.pipeline.shared.TileSpec + +/** + * View model for each tile that is available to be added/removed/moved in Edit mode. + * + * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in + * Quick Settings. + */ +class EditTileViewModel( + val tileSpec: TileSpec, + val icon: Icon, + val label: Text, + val appName: Text?, + val isCurrent: Boolean, + val availableEditActions: Set<AvailableEditActions>, +) + +enum class AvailableEditActions { + ADD, + REMOVE, + MOVE, +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt index cfcea9829610..c5b27376a82a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt @@ -23,6 +23,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ServiceInfo import android.os.UserHandle import android.service.quicksettings.TileService import androidx.annotation.GuardedBy @@ -36,14 +37,17 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn interface InstalledTilesComponentRepository { fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> + + fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> } @SysUISingleton @@ -55,38 +59,45 @@ constructor( private val packageChangeRepository: PackageChangeRepository ) : InstalledTilesComponentRepository { - @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>() + @GuardedBy("userMap") private val userMap = mutableMapOf<Int, StateFlow<List<ServiceInfo>>>() override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> = - synchronized(userMap) { - userMap.getOrPut(userId) { - /* - * In order to query [PackageManager] for different users, this implementation will - * call [Context.createContextAsUser] and retrieve the [PackageManager] from that - * context. - */ - val packageManager = - if (applicationContext.userId == userId) { - applicationContext.packageManager - } else { - applicationContext - .createContextAsUser( - UserHandle.of(userId), - /* flags */ 0, - ) - .packageManager - } - packageChangeRepository - .packageChanged(UserHandle.of(userId)) - .onStart { emit(PackageChangeModel.Empty) } - .map { reloadComponents(userId, packageManager) } - .distinctUntilChanged() - .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1) - } + synchronized(userMap) { getForUserLocked(userId) } + .map { it.mapTo(mutableSetOf()) { it.componentName } } + + override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> { + return synchronized(userMap) { getForUserLocked(userId).value } + } + + private fun getForUserLocked(userId: Int): StateFlow<List<ServiceInfo>> { + return userMap.getOrPut(userId) { + /* + * In order to query [PackageManager] for different users, this implementation will + * call [Context.createContextAsUser] and retrieve the [PackageManager] from that + * context. + */ + val packageManager = + if (applicationContext.userId == userId) { + applicationContext.packageManager + } else { + applicationContext + .createContextAsUser( + UserHandle.of(userId), + /* flags */ 0, + ) + .packageManager + } + packageChangeRepository + .packageChanged(UserHandle.of(userId)) + .onStart { emit(PackageChangeModel.Empty) } + .map { reloadComponents(userId, packageManager) } + .distinctUntilChanged() + .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList()) } + } @WorkerThread - private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> { + private fun reloadComponents(userId: Int, packageManager: PackageManager): List<ServiceInfo> { return packageManager .queryIntentServicesAsUser(INTENT, FLAGS, userId) .mapNotNull { it.serviceInfo } @@ -100,7 +111,6 @@ constructor( false } } - .mapTo(mutableSetOf()) { it.componentName } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt index 61896f0a3816..b7fcef4376ea 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt @@ -115,6 +115,10 @@ interface CurrentTilesInteractor : ProtoDumpable { * @see TileSpecRepository.setTiles */ fun setTiles(specs: List<TileSpec>) + + companion object { + val POSITION_AT_END: Int = TileSpecRepository.POSITION_AT_END + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt new file mode 100644 index 000000000000..2ae3f07d6b67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.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.qs.pipeline.domain.interactor + +import com.android.systemui.qs.pipeline.data.repository.MinimumTilesRepository +import javax.inject.Inject + +class MinimumTilesInteractor +@Inject +constructor( + private val minimumTilesRepository: MinimumTilesRepository, +) { + val minNumberOfTiles: Int + get() = minimumTilesRepository.minNumberOfTiles +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index c24113f14f00..56588ff75a5a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -55,6 +55,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.Expandable; @@ -632,12 +633,23 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy } public static class DrawableIcon extends Icon { + protected final Drawable mDrawable; protected final Drawable mInvisibleDrawable; + private static final String TAG = "QSTileImpl"; public DrawableIcon(Drawable drawable) { mDrawable = drawable; - mInvisibleDrawable = drawable.getConstantState().newDrawable(); + Drawable.ConstantState nullableConstantState = drawable.getConstantState(); + if (nullableConstantState == null) { + if (!(drawable instanceof SignalDrawable)) { + Log.w(TAG, "DrawableIcon: drawable has null ConstantState" + + " and is not a SignalDrawable"); + } + mInvisibleDrawable = drawable; + } else { + mInvisibleDrawable = nullableConstantState.newDrawable(); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index f3852a275209..4fd0df4d3f8f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -186,7 +186,8 @@ open class QSTileViewImpl @JvmOverloads constructor( private val locInScreen = IntArray(2) /** Visuo-haptic long-press effects */ - private var haveLongPressPropertiesBeenReset = true + var haveLongPressPropertiesBeenReset = true + private set private var paddingForLaunch = Rect() private var initialLongPressProperties: QSLongPressProperties? = null private var finalLongPressProperties: QSLongPressProperties? = null @@ -772,7 +773,11 @@ open class QSTileViewImpl @JvmOverloads constructor( } } - override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties() + override fun onActivityLaunchAnimationEnd() { + if (longPressEffect != null && !haveLongPressPropertiesBeenReset) { + resetLongPressEffectProperties() + } + } fun prepareForLaunch() { val startingHeight = initialLongPressProperties?.height?.toInt() ?: 0 @@ -877,8 +882,8 @@ open class QSTileViewImpl @JvmOverloads constructor( background.updateBounds( left = 0, top = 0, - right = initialLongPressProperties?.width?.toInt() ?: 0, - bottom = initialLongPressProperties?.height?.toInt() ?: 0, + right = initialLongPressProperties?.width?.toInt() ?: measuredWidth, + bottom = initialLongPressProperties?.height?.toInt() ?: measuredHeight, ) changeCornerRadius(resources.getDimensionPixelSize(R.dimen.qs_corner_radius).toFloat()) setAllColors( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index b88c1e566c4a..5346b237111f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -201,6 +201,7 @@ constructor( qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId + override fun getTileLabel(): CharSequence = with(qsTileViewModel.config.uiConfig) { when (this) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index d6325c049823..a04fa3817493 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel import javax.inject.Inject @@ -27,4 +28,5 @@ class QuickSettingsContainerViewModel constructor( val brightnessSliderViewModel: BrightnessSliderViewModel, val tileGridViewModel: TileGridViewModel, + val editModeViewModel: EditModeViewModel, ) diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 76bd80ff6f79..faf2bbc1ba3b 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -707,7 +707,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop) { if (mOverviewProxy != null) { try { - if (DesktopModeStatus.isEnabled() && (sysUiState.getFlags() + if (DesktopModeStatus.canEnterDesktopMode(mContext) + && (sysUiState.getFlags() & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt index b91dd0451808..0603d21fa2a6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt @@ -24,6 +24,8 @@ import com.android.compose.animation.scene.TransitionKey * These are the subset of transitions that can be referenced by key when asking for a scene change. */ object TransitionKeys { + /** Reference to the gone to shade transition with split shade enabled. */ + val GoneToSplitShade = TransitionKey("GoneToSplitShade") /** Reference to a scene transition that can collapse the shade scene instantly. */ val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly") diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 78704e16b586..c20d577d66a1 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -198,7 +198,7 @@ object SceneWindowRootViewBinder { private fun getDisplayWidth(context: Context): Dp { val point = Point() checkNotNull(context.display).getRealSize(point) - return point.x.dp + return point.x.toDp(context) } // TODO(b/298525212): remove once Compose exposes window inset bounds. diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt index b0af7f9ce072..016fe572894c 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt @@ -24,6 +24,7 @@ import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode import javax.inject.Inject @@ -70,10 +71,11 @@ constructor( )] = UserActionResult(Scenes.QuickSettingsShade) } + val downSceneKey = + if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade + val downTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split } this[Swipe(direction = SwipeDirection.Down)] = - UserActionResult( - if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade - ) + UserActionResult(downSceneKey, downTransitionKey) } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 494fc9b8c683..bd90de230d5f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -47,7 +47,6 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; -import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Process; import android.os.UserHandle; @@ -208,8 +207,7 @@ public class ScreenshotController { @Nullable private final ScreenshotSoundController mScreenshotSoundController; private final PhoneWindow mWindow; - private final DisplayManager mDisplayManager; - private final int mDisplayId; + private final Display mDisplay; private final ScrollCaptureExecutor mScrollCaptureExecutor; private final ScreenshotNotificationSmartActionsProvider mScreenshotNotificationSmartActionsProvider; @@ -249,7 +247,6 @@ public class ScreenshotController { @AssistedInject ScreenshotController( Context context, - DisplayManager displayManager, WindowManager windowManager, FeatureFlags flags, ScreenshotViewProxy.Factory viewProxyFactory, @@ -271,12 +268,13 @@ public class ScreenshotController { AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, - @Assisted int displayId, + @Assisted Display display, @Assisted boolean showUIOnExternalDisplay ) { mScreenshotSmartActions = screenshotSmartActions; mActionsProviderFactory = actionsProviderFactory; - mNotificationsController = screenshotNotificationsControllerFactory.create(displayId); + mNotificationsController = screenshotNotificationsControllerFactory.create( + display.getDisplayId()); mUiEventLogger = uiEventLogger; mImageExporter = imageExporter; mImageCapture = imageCapture; @@ -290,11 +288,9 @@ public class ScreenshotController { mScreenshotHandler = timeoutHandler; mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); - - mDisplayId = displayId; - mDisplayManager = displayManager; + mDisplay = display; mWindowManager = windowManager; - final Context displayContext = context.createDisplayContext(getDisplay()); + final Context displayContext = context.createDisplayContext(display); mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mFlags = flags; mActionIntentExecutor = actionIntentExecutor; @@ -302,7 +298,7 @@ public class ScreenshotController { mMessageContainerController = messageContainerController; mAssistContentRequester = assistContentRequester; - mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId); + mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId()); mScreenshotHandler.setOnTimeoutRunnable(() -> { if (DEBUG_UI) { @@ -328,7 +324,7 @@ public class ScreenshotController { }); // Sound is only reproduced from the controller of the default display. - if (displayId == Display.DEFAULT_DISPLAY) { + if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) { mScreenshotSoundController = screenshotSoundController.get(); } else { mScreenshotSoundController = null; @@ -356,7 +352,7 @@ public class ScreenshotController { if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN && screenshot.getBitmap() == null) { Rect bounds = getFullScreenRect(); - screenshot.setBitmap(mImageCapture.captureDisplay(mDisplayId, bounds)); + screenshot.setBitmap(mImageCapture.captureDisplay(mDisplay.getDisplayId(), bounds)); screenshot.setScreenBounds(bounds); } @@ -459,7 +455,7 @@ public class ScreenshotController { } private boolean shouldShowUi() { - return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; + return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; } void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { @@ -618,7 +614,7 @@ public class ScreenshotController { private void requestScrollCapture(UserHandle owner) { mScrollCaptureExecutor.requestScrollCapture( - mDisplayId, + mDisplay.getDisplayId(), mWindow.getDecorView().getWindowToken(), (response) -> { mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, @@ -641,7 +637,8 @@ public class ScreenshotController { } mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0, response.getPackageName()); - Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId, getFullScreenRect()); + Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(), + getFullScreenRect()); if (newScreenshot == null) { Log.e(TAG, "Failed to capture current screenshot for scroll transition!"); return; @@ -819,7 +816,8 @@ public class ScreenshotController { private void saveScreenshotInBackground( ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) { ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor, - requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), mDisplayId); + requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), + mDisplay.getDisplayId()); future.addListener(() -> { try { ImageExporter.Result result = future.get(); @@ -861,7 +859,7 @@ public class ScreenshotController { data.mActionsReadyListener = actionsReadyListener; data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; data.owner = owner; - data.displayId = mDisplayId; + data.displayId = mDisplay.getDisplayId(); if (mSaveInBgTask != null) { // just log success/failure for the pre-existing screenshot @@ -986,13 +984,9 @@ public class ScreenshotController { } } - private Display getDisplay() { - return mDisplayManager.getDisplay(mDisplayId); - } - private Rect getFullScreenRect() { DisplayMetrics displayMetrics = new DisplayMetrics(); - getDisplay().getRealMetrics(displayMetrics); + mDisplay.getRealMetrics(displayMetrics); return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); } @@ -1026,12 +1020,12 @@ public class ScreenshotController { @AssistedFactory public interface Factory { /** - * Creates an instance of the controller for that specific displayId. + * Creates an instance of the controller for that specific display. * - * @param displayId: display to capture - * @param showUIOnExternalDisplay: Whether the UI should be shown if this is an external - * display. + * @param display display to capture + * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external + * display. */ - ScreenshotController create(int displayId, boolean showUIOnExternalDisplay); + ScreenshotController create(Display display, boolean showUIOnExternalDisplay); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt index e56a4f45d7aa..40d709d0ab25 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt @@ -68,11 +68,13 @@ constructor( onSaved: (Uri?) -> Unit, requestCallback: RequestCallback ) { - val displayIds = getDisplaysToScreenshot(screenshotRequest.type) + val displays = getDisplaysToScreenshot(screenshotRequest.type) val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback) - displayIds.forEach { displayId: Int -> + displays.forEach { display -> + val displayId = display.displayId Log.d(TAG, "Executing screenshot for display $displayId") dispatchToController( + display = display, rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId), onSaved = if (displayId == Display.DEFAULT_DISPLAY) { @@ -85,6 +87,7 @@ constructor( /** All logging should be triggered only by this method. */ private suspend fun dispatchToController( + display: Display, rawScreenshotData: ScreenshotData, onSaved: (Uri?) -> Unit, callback: RequestCallback @@ -104,8 +107,7 @@ constructor( logScreenshotRequested(screenshotData) Log.d(TAG, "Screenshot request: $screenshotData") try { - getScreenshotController(screenshotData.displayId) - .handleScreenshot(screenshotData, onSaved, callback) + getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback) } catch (e: IllegalStateException) { Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e) onFailedScreenshotRequest(screenshotData, callback) @@ -135,12 +137,13 @@ constructor( callback.reportError() } - private suspend fun getDisplaysToScreenshot(requestType: Int): List<Int> { + private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> { + val allDisplays = displays.first() return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) { // If this is a provided image, let's show the UI on the default display only. - listOf(Display.DEFAULT_DISPLAY) + allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY } } else { - displays.first().filter { it.type in ALLOWED_DISPLAY_TYPES }.map { it.displayId } + allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES } } } @@ -170,9 +173,9 @@ constructor( screenshotControllers.clear() } - private fun getScreenshotController(id: Int): ScreenshotController { - return screenshotControllers.computeIfAbsent(id) { - screenshotControllerFactory.create(id, /* showUIOnExternalDisplay= */ false) + private fun getScreenshotController(display: Display): ScreenshotController { + return screenshotControllers.computeIfAbsent(display.displayId) { + screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 281857fb0658..6367d44bd439 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -51,9 +51,9 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.phone.SystemUIDialogFactory -import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.BooleanFlowOperators.not -import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -145,7 +145,7 @@ constructor( /** Returns a flow that tracks whether communal hub is available. */ fun communalAvailable(): Flow<Boolean> = - or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) /** * Creates the container view containing the glanceable hub UI. @@ -248,7 +248,7 @@ constructor( // transition to the bouncer would be incorrectly intercepted by the hub. collectFlow( containerView, - or( + anyOf( keyguardInteractor.primaryBouncerShowing, keyguardInteractor.alternateBouncerShowing ), @@ -267,7 +267,7 @@ constructor( ) collectFlow( containerView, - and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), + allOf(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), { shadeShowing = it updateTouchHandlingState() diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index ac76becd1797..d15a488cb582 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -33,6 +33,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -152,11 +153,13 @@ constructor( else -> Scenes.Lockscreen } + val upTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split } + val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single } return buildMap { if (!isCustomizing) { - this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + this[Swipe(SwipeDirection.Up)] = UserActionResult(up, upTransitionKey) } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 222b070d151d..14e14f4bd47f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -14,7 +14,6 @@ import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.ExpandHelper -import com.android.systemui.Flags.nsslFalsingFix import com.android.systemui.Gefingerpoken import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy import com.android.systemui.classifier.Classifier @@ -884,9 +883,7 @@ class DragDownHelper( isDraggingDown = false isTrackpadReverseScroll = false shadeRepository.setLegacyLockscreenShadeTracking(false) - if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) { - return true - } + return true } else { stopDragging() return false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java index 0c341cc92f83..ec3c7d0d6de4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java @@ -27,6 +27,9 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow * (e.g. clicking on a notification, tapping on the settings icon in the notification guts) */ public interface NotificationActivityStarter { + /** Called when the user clicks on the notification bubble icon. */ + void onNotificationBubbleIconClicked(NotificationEntry entry); + /** Called when the user clicks on the surface of a notification. */ void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index d10fac6ea3fc..6487d55197e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -117,11 +117,14 @@ public final class NotificationClicker implements View.OnClickListener { Notification notification = sbn.getNotification(); if (notification.contentIntent != null || notification.fullScreenIntent != null || row.getEntry().isBubble()) { + row.setBubbleClickListener(v -> + mNotificationActivityStarter.onNotificationBubbleIconClicked(row.getEntry())); row.setOnClickListener(this); row.setOnDragSuccessListener(mOnDragSuccessListener); } else { row.setOnClickListener(null); row.setOnDragSuccessListener(null); + row.setBubbleClickListener(null); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index 61cdea190a43..6a38f8df4715 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -38,6 +38,7 @@ import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; import com.android.systemui.res.R; +import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; @@ -745,6 +746,12 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView } } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + return TouchLogger.logDispatchTouch( + getClass().getSimpleName(), ev, super.dispatchTouchEvent(ev)); + } + /** * SourceType which should be reset when this View is detached * @param sourceType will be reset on View detached diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 5e3df7b5e60f..23c0a0db04d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -375,6 +375,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView }; private OnClickListener mOnClickListener; + @Nullable + private OnClickListener mBubbleClickListener; private OnDragSuccessListener mOnDragSuccessListener; private boolean mHeadsupDisappearRunning; private View mChildAfterViewWhenDismissed; @@ -1234,14 +1236,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * The click listener for the bubble button. */ + @Nullable public View.OnClickListener getBubbleClickListener() { - return v -> { - if (mBubblesManagerOptional.isPresent()) { - mBubblesManagerOptional.get() - .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */); - } - mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */); - }; + return mBubbleClickListener; + } + + /** + * Sets the click listener for the bubble button. + */ + public void setBubbleClickListener(@Nullable OnClickListener l) { + mBubbleClickListener = l; + // ensure listener is passed to the content views + mPrivateLayout.updateBubbleButton(mEntry); + mPublicLayout.updateBubbleButton(mEntry); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt index 9a54de1481a0..2527af87728e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.ui.viewbinder +import android.util.Log import android.view.MotionEvent import android.view.View import android.view.View.OnTouchListener @@ -72,7 +73,6 @@ private class TouchHandler( var isTouchEnabled = false override fun onTouch(v: View, ev: MotionEvent): Boolean { - val result = false if (ev.action == MotionEvent.ACTION_UP) { view.setLastActionUpTime(ev.eventTime) } @@ -82,13 +82,22 @@ private class TouchHandler( } if (ev.action == MotionEvent.ACTION_UP) { // If this is a false tap, capture the even so it doesn't result in a click. - return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) + return falsingManager.isFalseTap(FalsingManager.LOW_PENALTY).also { + if (it) { + Log.d(v::class.simpleName ?: TAG, "capturing false tap") + } + } } - return result + return false } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false /** Use [onTouch] instead. */ override fun onTouchEvent(ev: MotionEvent): Boolean = false + + companion object { + private const val TAG = "ActivatableNotificationViewBinder" + } } + 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 773a6bf752a6..232b4e993f06 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 @@ -1469,9 +1469,10 @@ public class NotificationStackScrollLayout public void setExpandedHeight(float height) { final boolean skipHeightUpdate = shouldSkipHeightUpdate(); - // when scene framework is enabled, updateStackPosition is already called by - // updateTopPadding every time the stack moves, so skip it here to avoid flickering. - if (!SceneContainerFlag.isEnabled()) { + // when scene framework is enabled and in single shade, updateStackPosition is already + // called by updateTopPadding every time the stack moves, so skip it here to avoid + // flickering. + if (!SceneContainerFlag.isEnabled() || mShouldUseSplitNotificationShade) { updateStackPosition(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 3011bc284961..c1c63cdec448 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -23,7 +23,6 @@ import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; -import static com.android.systemui.Flags.nsslFalsingFix; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; @@ -2062,32 +2061,18 @@ public class NotificationStackScrollLayoutController implements Dumpable { } boolean horizontalSwipeWantsIt = false; boolean scrollerWantsIt = false; - if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) { - // Reverse the order relative to the else statement. onScrollTouch will reset on an - // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes. - if (mLongPressedView == null && !mView.isBeingDragged() - && !expandingNotification - && !mView.getExpandedInThisMotion() - && !onlyScrollingInThisMotion - && !mView.getDisallowDismissInThisMotion()) { - horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); - } - if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping() - && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) { - scrollerWantsIt = mView.onScrollTouch(ev); - } - } else { - if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping() - && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) { - scrollerWantsIt = mView.onScrollTouch(ev); - } - if (mLongPressedView == null && !mView.isBeingDragged() - && !expandingNotification - && !mView.getExpandedInThisMotion() - && !onlyScrollingInThisMotion - && !mView.getDisallowDismissInThisMotion()) { - horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); - } + // NOTE: the order of these is important. If reversed, onScrollTouch will reset on an + // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes. + if (mLongPressedView == null && !mView.isBeingDragged() + && !expandingNotification + && !mView.getExpandedInThisMotion() + && !onlyScrollingInThisMotion + && !mView.getDisallowDismissInThisMotion()) { + horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); + } + if (mLongPressedView == null && mView.isExpanded() && !mSwipeHelper.isSwiping() + && !expandingNotification && !mView.getDisallowScrollingInThisMotion()) { + scrollerWantsIt = mView.onScrollTouch(ev); } // Check if we need to clear any snooze leavebehinds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 1f1251a37f58..0ba7b3c214c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -68,8 +68,8 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor -import com.android.systemui.util.kotlin.BooleanFlowOperators.and -import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import javax.inject.Inject @@ -246,7 +246,7 @@ constructor( keyguardTransitionInteractor.finishedKeyguardState.map { state -> state == GLANCEABLE_HUB }, - or( + anyOf( keyguardTransitionInteractor.isInTransitionToState(GLANCEABLE_HUB), keyguardTransitionInteractor.isInTransitionFromState(GLANCEABLE_HUB), ), @@ -424,14 +424,14 @@ constructor( while (currentCoroutineContext().isActive) { emit(false) // Ensure states are inactive to start - and( + allOf( *toFlowArray(statesForHiddenKeyguard) { state -> keyguardTransitionInteractor.transitionValue(state).map { it == 0f } } ) .first { it } // Wait for a qualifying transition to begin - or( + anyOf( *toFlowArray(statesForHiddenKeyguard) { state -> keyguardTransitionInteractor .transitionStepsToState(state) @@ -446,7 +446,7 @@ constructor( // it is considered safe to reset alpha to 1f for HUNs. combine( keyguardInteractor.statusBarState, - and( + allOf( *toFlowArray(statesForHiddenKeyguard) { state -> keyguardTransitionInteractor.transitionValue(state).map { it == 0f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index e1a7f22a913a..e92058bf034a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -96,6 +96,20 @@ import javax.inject.Inject; @SysUISingleton public class StatusBarNotificationActivityStarter implements NotificationActivityStarter { + /** + * Helps to avoid recalculation of values provided to + * {@link #onDismiss(PendingIntent, boolean, boolean, boolean)}} method + */ + private interface OnKeyguardDismissedAction { + /** + * Invoked when keyguard is dismissed + * + * @return is used as return value for {@link ActivityStarter.OnDismissAction#onDismiss()} + */ + boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animate, + boolean showOverTheLockScreen); + } + private final Context mContext; private final int mDisplayId; @@ -207,6 +221,30 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } /** + * Called when the user clicks on the notification bubble icon. + * + * @param entry notification that bubble icon was clicked + */ + @Override + public void onNotificationBubbleIconClicked(NotificationEntry entry) { + Runnable action = () -> { + mBubblesManagerOptional.ifPresent(bubblesManager -> + bubblesManager.onUserChangedBubble(entry, !entry.isBubble())); + mHeadsUpManager.removeNotification(entry.getKey(), /* releaseImmediately= */ true); + }; + if (entry.isBubble()) { + // entry is being un-bubbled, no need to unlock + action.run(); + } else { + performActionAfterKeyguardDismissed(entry, + (intent, isActivityIntent, animate, showOverTheLockScreen) -> { + action.run(); + return false; + }); + } + } + + /** * Called when a notification is clicked. * * @param entry notification that was clicked @@ -217,7 +255,15 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(), mKeyguardStateController.isVisible(), mNotificationShadeWindowController.getPanelExpanded()); + OnKeyguardDismissedAction action = + (intent, isActivityIntent, animate, showOverTheLockScreen) -> + performActionOnKeyguardDismissed(entry, row, intent, isActivityIntent, + animate, showOverTheLockScreen); + performActionAfterKeyguardDismissed(entry, action); + } + private void performActionAfterKeyguardDismissed(NotificationEntry entry, + OnKeyguardDismissedAction action) { if (mRemoteInputManager.isRemoteInputActive(entry)) { // We have an active remote input typed and the user clicked on the notification. // this was probably unintentional, so we're closing the edit text instead. @@ -251,8 +297,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() { @Override public boolean onDismiss() { - return handleNotificationClickAfterKeyguardDismissed( - entry, row, intent, isActivityIntent, animate, showOverLockscreen); + return action.onDismiss(intent, isActivityIntent, animate, showOverLockscreen); } @Override @@ -271,7 +316,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } } - private boolean handleNotificationClickAfterKeyguardDismissed( + private boolean performActionOnKeyguardDismissed( NotificationEntry entry, ExpandableNotificationRow row, PendingIntent intent, @@ -282,7 +327,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed( entry, row, intent, isActivityIntent, animate); - if (showOverLockscreen) { mShadeController.addPostCollapseAction(runnable); mShadeController.collapseShade(true /* animate */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 226a84a78116..88ca9e5f1744 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -229,7 +229,7 @@ abstract class StatusBarPipelineModule { @SysUISingleton @OemSatelliteInputLog fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer { - return factory.create("DeviceBasedSatelliteInputLog", 32) + return factory.create("DeviceBasedSatelliteInputLog", 150) } const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index a7c4187afdbd..12f252d215a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -249,11 +249,17 @@ constructor( try { sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) registered = true + logBuffer.i { "Registered for signal strength successfully" } } catch (e: Exception) { logBuffer.e("error registering for signal strength", e) } - awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) } + awaitClose { + if (registered) { + sm.unregisterForNtnSignalStrengthChanged(cb) + logBuffer.i { "Unregistered for signal strength successfully" } + } + } } .flowOn(bgDispatcher) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 37be1c6aa73d..a817b31070a1 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.user.data.repository import android.annotation.SuppressLint +import android.annotation.UserIdInt import android.content.Context import android.content.pm.UserInfo import android.os.UserHandle @@ -107,6 +108,22 @@ interface UserRepository { fun isSimpleUserSwitcher(): Boolean fun isUserSwitcherEnabled(): Boolean + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt suspend fun getMainUserId(): Int? } @SysUISingleton @@ -239,6 +256,10 @@ constructor( return _userSwitcherSettings.value.isUserSwitcherEnabled } + override suspend fun getMainUserId(): Int? { + return withContext(backgroundDispatcher) { manager.mainUser?.identifier } + } + private suspend fun getSettings(): UserSwitcherSettingsModel { return withContext(backgroundDispatcher) { val isSimpleUserSwitcher = diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt index 38b381ac543e..59c819d41493 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt @@ -2,6 +2,7 @@ package com.android.systemui.user.domain.interactor import android.annotation.UserIdInt import android.content.pm.UserInfo +import android.os.UserManager import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags.refactorGetCurrentUser import com.android.systemui.dagger.SysUISingleton @@ -38,4 +39,23 @@ class SelectedUserInteractor @Inject constructor(private val repository: UserRep KeyguardUpdateMonitor.getCurrentUser() } } + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt + fun getMainUserId(): Int? { + return repository.mainUserId + } } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt index b3008856d370..a2759c6bf470 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt @@ -28,11 +28,23 @@ object BooleanFlowOperators { * * Usage: * ``` - * val result = and(flow1, flow2) + * val result = allOf(flow1, flow2) * ``` */ - fun and(vararg flows: Flow<Boolean>): Flow<Boolean> = - combine(flows.asIterable()) { values -> values.all { it } }.distinctUntilChanged() + fun allOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().all() + + /** + * Logical AND operator for boolean flows. Will collect all flows and [combine] them to + * determine the result. + */ + fun Array<Flow<Boolean>>.all(): Flow<Boolean> = allOf(*this) + + /** + * Logical AND operator for boolean flows. Will collect all flows and [combine] them to + * determine the result. + */ + fun Iterable<Flow<Boolean>>.all(): Flow<Boolean> = + combine(this) { values -> values.all { it } }.distinctUntilChanged() /** * Logical NOT operator for a boolean flow. @@ -48,6 +60,36 @@ object BooleanFlowOperators { * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to * determine the result. */ - fun or(vararg flows: Flow<Boolean>): Flow<Boolean> = - combine(flows.asIterable()) { values -> values.any { it } }.distinctUntilChanged() + fun anyOf(vararg flows: Flow<Boolean>): Flow<Boolean> = flows.asIterable().any() + + /** + * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to + * determine the result. + */ + fun Array<Flow<Boolean>>.any(): Flow<Boolean> = anyOf(*this) + + /** + * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to + * determine the result. + */ + fun Iterable<Flow<Boolean>>.any(): Flow<Boolean> = + combine(this) { values -> values.any { it } }.distinctUntilChanged() + + /** + * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise + * produces `false`. + */ + fun noneOf(vararg flows: Flow<Boolean>): Flow<Boolean> = not(anyOf(*flows)) + + /** + * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise + * produces `false`. + */ + fun Array<Flow<Boolean>>.none(): Flow<Boolean> = noneOf(*this) + + /** + * Returns a Flow that produces `true` when all input flows are producing `false`, otherwise + * produces `false`. + */ + fun Iterable<Flow<Boolean>>.none(): Flow<Boolean> = not(any()) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt index af1d31528675..f62a55d02f61 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt @@ -77,6 +77,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor + @Mock private lateinit var deviceItemActionInteractor: DeviceItemActionInteractor + @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator @@ -118,6 +120,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, + deviceItemActionInteractor, BluetoothStateInteractor( localBluetoothManager, bluetoothTileDialogLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt new file mode 100644 index 000000000000..762137bede27 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorImplTest.kt @@ -0,0 +1,121 @@ +/* + * 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.bluetooth.qsdialog + +import android.bluetooth.BluetoothDevice +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +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.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class DeviceItemActionInteractorImplTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } + private lateinit var actionInteractorImpl: DeviceItemActionInteractor + + @Mock private lateinit var dialog: SystemUIDialog + @Mock private lateinit var cachedDevice: CachedBluetoothDevice + @Mock private lateinit var device: BluetoothDevice + @Mock private lateinit var deviceItem: DeviceItem + + @Before + fun setUp() { + actionInteractorImpl = kosmos.deviceItemActionInteractor + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedDevice) + whenever(cachedDevice.address).thenReturn("ADDRESS") + whenever(cachedDevice.device).thenReturn(device) + } + + @Test + fun testOnClick_connectedMedia_setActive() { + with(kosmos) { + testScope.runTest { + whenever(deviceItem.type) + .thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) + actionInteractorImpl.onClick(deviceItem, dialog) + verify(cachedDevice).setActive() + verify(bluetoothTileDialogLogger) + .logDeviceClick( + cachedDevice.address, + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE + ) + } + } + } + + @Test + fun testOnClick_activeMedia_disconnect() { + with(kosmos) { + testScope.runTest { + whenever(deviceItem.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) + actionInteractorImpl.onClick(deviceItem, dialog) + verify(cachedDevice).disconnect() + verify(bluetoothTileDialogLogger) + .logDeviceClick( + cachedDevice.address, + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE + ) + } + } + } + + @Test + fun testOnClick_connectedOtherDevice_disconnect() { + with(kosmos) { + testScope.runTest { + whenever(deviceItem.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) + actionInteractorImpl.onClick(deviceItem, dialog) + verify(cachedDevice).disconnect() + verify(bluetoothTileDialogLogger) + .logDeviceClick(cachedDevice.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) + } + } + } + + @Test + fun testOnClick_saved_connect() { + with(kosmos) { + testScope.runTest { + whenever(deviceItem.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE) + actionInteractorImpl.onClick(deviceItem, dialog) + verify(cachedDevice).connect() + verify(bluetoothTileDialogLogger) + .logDeviceClick(cachedDevice.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE) + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt new file mode 100644 index 000000000000..e8e37bc81866 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.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.bluetooth.qsdialog + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.util.mockito.mock + +val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} } + +val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by + Kosmos.Fixture { + DeviceItemActionInteractorImpl( + testDispatcher, + bluetoothTileDialogLogger, + uiEventLogger, + ) + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt index daf4a3cbb9de..2b4f9503f371 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt @@ -23,7 +23,6 @@ import android.media.AudioManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest -import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.SysuiTestCase @@ -39,7 +38,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -71,8 +69,6 @@ class DeviceItemInteractorTest : SysuiTestCase() { @Mock private lateinit var localBluetoothManager: LocalBluetoothManager - @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var logger: BluetoothTileDialogLogger private val fakeSystemClock = FakeSystemClock() @@ -94,7 +90,6 @@ class DeviceItemInteractorTest : SysuiTestCase() { adapter, localBluetoothManager, fakeSystemClock, - uiEventLogger, logger, testScope.backgroundScope, dispatcher @@ -218,61 +213,6 @@ class DeviceItemInteractorTest : SysuiTestCase() { } } - @Test - fun testUpdateDeviceItemOnClick_connectedMedia_setActive() { - testScope.runTest { - `when`(deviceItem1.type).thenReturn(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) - - interactor.updateDeviceItemOnClick(deviceItem1) - - verify(cachedDevice1).setActive() - verify(logger) - .logDeviceClick( - cachedDevice1.address, - DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE - ) - } - } - - @Test - fun testUpdateDeviceItemOnClick_activeMedia_disconnect() { - testScope.runTest { - `when`(deviceItem1.type).thenReturn(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) - - interactor.updateDeviceItemOnClick(deviceItem1) - - verify(cachedDevice1).disconnect() - verify(logger) - .logDeviceClick(cachedDevice1.address, DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) - } - } - - @Test - fun testUpdateDeviceItemOnClick_connectedOtherDevice_disconnect() { - testScope.runTest { - `when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - - interactor.updateDeviceItemOnClick(deviceItem1) - - verify(cachedDevice1).disconnect() - verify(logger) - .logDeviceClick(cachedDevice1.address, DeviceItemType.CONNECTED_BLUETOOTH_DEVICE) - } - } - - @Test - fun testUpdateDeviceItemOnClick_saved_connect() { - testScope.runTest { - `when`(deviceItem1.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE) - - interactor.updateDeviceItemOnClick(deviceItem1) - - verify(cachedDevice1).connect() - verify(logger) - .logDeviceClick(cachedDevice1.address, DeviceItemType.SAVED_BLUETOOTH_DEVICE) - } - } - private fun createFactory( isFilterMatchFunc: (CachedBluetoothDevice) -> Boolean, deviceItem: DeviceItem diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt new file mode 100644 index 000000000000..05a2ca20037b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt @@ -0,0 +1,132 @@ +/* + * 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.keyboard.shortcut.ui + +import android.content.Intent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity +import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class ShortcutHelperActivityStarterTest : SysuiTestCase() { + + private val kosmos = + Kosmos().also { + it.testCase = this + it.testDispatcher = UnconfinedTestDispatcher() + } + + private val testScope = kosmos.testScope + private val testHelper = kosmos.shortcutHelperTestHelper + private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity + private val starter = kosmos.shortcutHelperActivityStarter + + @Test + fun start_doesNotStartByDefault() = + testScope.runTest { + starter.start() + + assertThat(fakeStartActivity.startIntents).isEmpty() + } + + @Test + fun start_onToggle_startsActivity() = + testScope.runTest { + starter.start() + + testHelper.toggle(deviceId = 456) + + verifyShortcutHelperActivityStarted() + } + + @Test + fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() = + testScope.runTest { + starter.start() + + testHelper.toggle(deviceId = 456) + testHelper.toggle(deviceId = 456) + testHelper.toggle(deviceId = 456) + testHelper.toggle(deviceId = 456) + + verifyShortcutHelperActivityStarted(numTimes = 2) + } + + @Test + fun start_onRequestShowShortcuts_startsActivity() = + testScope.runTest { + starter.start() + + testHelper.showFromActivity() + + verifyShortcutHelperActivityStarted() + } + + @Test + fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() = + testScope.runTest { + starter.start() + + testHelper.showFromActivity() + testHelper.showFromActivity() + testHelper.showFromActivity() + + verifyShortcutHelperActivityStarted(numTimes = 1) + } + + @Test + fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() = + testScope.runTest { + starter.start() + + testHelper.hideFromActivity() + testHelper.hideForSystem() + testHelper.toggle(deviceId = 987) + testHelper.showFromActivity() + testHelper.hideFromActivity() + testHelper.hideForSystem() + testHelper.toggle(deviceId = 456) + testHelper.showFromActivity() + + verifyShortcutHelperActivityStarted(numTimes = 2) + } + + private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) { + assertThat(fakeStartActivity.startIntents).hasSize(numTimes) + fakeStartActivity.startIntents.forEach { intent -> + assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK) + assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java))) + .isTrue() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt new file mode 100644 index 000000000000..86533086f67a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -0,0 +1,130 @@ +/* + * 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.keyboard.shortcut.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class ShortcutHelperViewModelTest : SysuiTestCase() { + + private val kosmos = + Kosmos().also { + it.testCase = this + it.testDispatcher = UnconfinedTestDispatcher() + } + + private val testScope = kosmos.testScope + private val testHelper = kosmos.shortcutHelperTestHelper + + private val viewModel = kosmos.shortcutHelperViewModel + + @Test + fun shouldShow_falseByDefault() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + assertThat(shouldShow).isFalse() + } + + @Test + fun shouldShow_trueAfterShowRequested() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + testHelper.showFromActivity() + + assertThat(shouldShow).isTrue() + } + + @Test + fun shouldShow_trueAfterToggleRequested() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + testHelper.toggle(deviceId = 123) + + assertThat(shouldShow).isTrue() + } + + @Test + fun shouldShow_falseAfterToggleTwice() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + testHelper.toggle(deviceId = 123) + testHelper.toggle(deviceId = 123) + + assertThat(shouldShow).isFalse() + } + + @Test + fun shouldShow_falseAfterViewDestroyed() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + testHelper.toggle(deviceId = 567) + viewModel.onUserLeave() + + assertThat(shouldShow).isFalse() + } + + @Test + fun shouldShow_doesNotEmitDuplicateValues() = + testScope.runTest { + val shouldShowValues by collectValues(viewModel.shouldShow) + + testHelper.hideForSystem() + testHelper.toggle(deviceId = 987) + testHelper.showFromActivity() + viewModel.onUserLeave() + testHelper.hideFromActivity() + testHelper.hideForSystem() + testHelper.toggle(deviceId = 456) + testHelper.showFromActivity() + + assertThat(shouldShowValues).containsExactly(false, true, false, true).inOrder() + } + + @Test + fun shouldShow_emitsLatestValueToNewSubscribers() = + testScope.runTest { + val shouldShow by collectLastValue(viewModel.shouldShow) + + testHelper.showFromActivity() + + val shouldShowNew by collectLastValue(viewModel.shouldShow) + assertThat(shouldShowNew).isEqualTo(shouldShow) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt index bcaad01f1a24..f5b5261de139 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt @@ -19,24 +19,20 @@ package com.android.systemui.keyguard.data.repository -import android.os.fakeExecutorHandler import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepository -import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint -import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT +import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.ThreadAssert -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -50,31 +46,32 @@ import org.mockito.MockitoAnnotations class KeyguardBlueprintRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardBlueprintRepository @Mock lateinit var configurationRepository: ConfigurationRepository - @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint @Mock lateinit var threadAssert: ThreadAssert + private val testScope = TestScope(StandardTestDispatcher()) private val kosmos: Kosmos = testKosmos() @Before fun setup() { MockitoAnnotations.initMocks(this) - with(kosmos) { - whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT) - underTest = - KeyguardBlueprintRepository( - setOf(defaultLockscreenBlueprint), - fakeExecutorHandler, - threadAssert, - ) - } + underTest = kosmos.keyguardBlueprintRepository } @Test fun testApplyBlueprint_DefaultLayout() { testScope.runTest { val blueprint by collectLastValue(underTest.blueprint) - underTest.applyBlueprint(defaultLockscreenBlueprint) - assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint) + underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT) + assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint) + } + } + + @Test + fun testApplyBlueprint_SplitShadeLayout() { + testScope.runTest { + val blueprint by collectLastValue(underTest.blueprint) + underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID) + assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint) } } @@ -83,33 +80,22 @@ class KeyguardBlueprintRepositoryTest : SysuiTestCase() { testScope.runTest { val blueprint by collectLastValue(underTest.blueprint) underTest.refreshBlueprint() - assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint) + assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint) } } @Test - fun testTransitionToLayout_validId() { - assertThat(underTest.applyBlueprint(DEFAULT)).isTrue() + fun testTransitionToDefaultLayout_validId() { + assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue() } @Test - fun testTransitionToLayout_invalidId() { - assertThat(underTest.applyBlueprint("abc")).isFalse() + fun testTransitionToSplitShadeLayout_validId() { + assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue() } @Test - fun testTransitionToSameBlueprint_refreshesBlueprint() = - with(kosmos) { - testScope.runTest { - val transition by collectLastValue(underTest.refreshTransition) - fakeExecutor.runAllReady() - runCurrent() - - underTest.applyBlueprint(defaultLockscreenBlueprint) - fakeExecutor.runAllReady() - runCurrent() - - assertThat(transition).isNotNull() - } - } + fun testTransitionToLayout_invalidId() { + assertThat(underTest.applyBlueprint("abc")).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt index ac5823e9365b..0bdf47a51670 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository +import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint import com.android.systemui.kosmos.testScope @@ -40,6 +41,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -54,7 +56,7 @@ import org.mockito.MockitoAnnotations class KeyguardBlueprintInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest by lazy { kosmos.keyguardBlueprintInteractor } + private val underTest = kosmos.keyguardBlueprintInteractor private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository } private val configurationRepository by lazy { kosmos.fakeConfigurationRepository } private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository } @@ -79,8 +81,9 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { val blueprintId by collectLastValue(underTest.blueprintId) kosmos.shadeRepository.setShadeMode(ShadeMode.Single) configurationRepository.onConfigurationChange() - runCurrent() + runCurrent() + advanceUntilIdle() assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.Companion.DEFAULT) } } @@ -92,8 +95,9 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { val blueprintId by collectLastValue(underTest.blueprintId) kosmos.shadeRepository.setShadeMode(ShadeMode.Split) configurationRepository.onConfigurationChange() - runCurrent() + runCurrent() + advanceUntilIdle() assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID) } } @@ -102,12 +106,13 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN) fun fingerprintPropertyInitialized_updatesBlueprint() { testScope.runTest { - val blueprintId by collectLastValue(underTest.blueprintId) - kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull() + fingerprintPropertyRepository.supportsUdfps() // initialize properties - runCurrent() - assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID) + runCurrent() + advanceUntilIdle() + assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull() } } @@ -119,9 +124,23 @@ class KeyguardBlueprintInteractorTest : SysuiTestCase() { kosmos.shadeRepository.setShadeMode(ShadeMode.Split) clockRepository.setCurrentClock(clockController) configurationRepository.onConfigurationChange() - runCurrent() + runCurrent() + advanceUntilIdle() assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.DEFAULT) } } + + @Test + fun testRefreshFromConfigChange() { + testScope.runTest { + assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull() + + configurationRepository.onConfigurationChange() + + runCurrent() + advanceUntilIdle() + assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull() + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt index dbf6a29073a6..8a0613f9b010 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt @@ -66,25 +66,19 @@ class KeyguardBlueprintCommandListenerTest : SysuiTestCase() { fun testHelp() { command().execute(pw, listOf("help")) verify(pw, atLeastOnce()).println(anyString()) - verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString()) + verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString()) } @Test fun testBlank() { command().execute(pw, listOf()) verify(pw, atLeastOnce()).println(anyString()) - verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString()) + verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString()) } @Test fun testValidArg() { command().execute(pw, listOf("fake")) - verify(keyguardBlueprintInteractor).transitionToBlueprint("fake") - } - - @Test - fun testValidArg_Int() { - command().execute(pw, listOf("1")) - verify(keyguardBlueprintInteractor).transitionToBlueprint(1) + verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt deleted file mode 100644 index 8cc3a85ef6c8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.panels.domain.repository - -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.qs.panels.data.repository.IconTilesRepositoryImpl -import com.android.systemui.qs.pipeline.shared.TileSpec -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class IconTilesRepositoryImplTest : SysuiTestCase() { - - private val underTest = IconTilesRepositoryImpl() - - @Test - fun iconTilesSpecsIsValid() = runTest { - val tilesSpecs by collectLastValue(underTest.iconTilesSpecs) - assertThat(tilesSpecs).isEqualTo(ICON_ONLY_TILES_SPECS) - } - - companion object { - private val ICON_ONLY_TILES_SPECS = - setOf( - TileSpec.create("airplane"), - TileSpec.create("battery"), - TileSpec.create("cameratoggle"), - TileSpec.create("cast"), - TileSpec.create("color_correction"), - TileSpec.create("inversion"), - TileSpec.create("saver"), - TileSpec.create("dnd"), - TileSpec.create("flashlight"), - TileSpec.create("location"), - TileSpec.create("mictoggle"), - TileSpec.create("nfc"), - TileSpec.create("night"), - TileSpec.create("rotation") - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt index b5ef8c26a7ce..db11c3e89160 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt @@ -500,6 +500,42 @@ class QSTileViewImplTest : SysuiTestCase() { ) } + @Test + fun onActivityLaunchAnimationEnd_onFreshTile_longPressPropertiesAreReset() { + // WHEN an activity launch animation ends on a fresh tile + tileView.onActivityLaunchAnimationEnd() + + // THEN the tile's long-press effect properties are reset by default + assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue() + } + + @Test + fun onUpdateLongPressEffectProperties_duringLongPressEffect_propertiesAreNotReset() { + // GIVEN a state that supports long-press + val state = QSTile.State() + tileView.changeState(state) + + // WHEN the long-press effect is updating the properties + tileView.updateLongPressEffectProperties(1f) + + // THEN the tile's long-press effect properties haven't reset + assertThat(tileView.haveLongPressPropertiesBeenReset).isFalse() + } + + @Test + fun onActivityLaunchAnimationEnd_afterLongPressEffect_longPressPropertiesAreReset() { + // GIVEN a state that supports long-press and the long-press effect updating + val state = QSTile.State() + tileView.changeState(state) + tileView.updateLongPressEffectProperties(1f) + + // WHEN an activity launch animation ends on a fresh tile + tileView.onActivityLaunchAnimationEnd() + + // THEN the tile's long-press effect properties are reset + assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue() + } + class FakeTileView( context: Context, collapsed: Boolean, diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt index 0f3714385725..bf7d909380df 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt @@ -69,8 +69,9 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { @Before fun setUp() { - whenever(controllerFactory.create(eq(0), any())).thenReturn(controller0) - whenever(controllerFactory.create(eq(1), any())).thenReturn(controller1) + whenever(controllerFactory.create(any(), any())).thenAnswer { + if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1 + } whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0) whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1) } @@ -78,12 +79,14 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { @Test fun executeScreenshots_severalDisplays_callsControllerForEachOne() = testScope.runTest { - setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1)) + val internalDisplay = display(TYPE_INTERNAL, id = 0) + val externalDisplay = display(TYPE_EXTERNAL, id = 1) + setDisplays(internalDisplay, externalDisplay) val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback) - verify(controllerFactory).create(eq(0), any()) - verify(controllerFactory).create(eq(1), any()) + verify(controllerFactory).create(eq(internalDisplay), any()) + verify(controllerFactory).create(eq(externalDisplay), any()) val capturer = ArgumentCaptor<ScreenshotData>() @@ -107,7 +110,9 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { @Test fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() = testScope.runTest { - setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1)) + val internalDisplay = display(TYPE_INTERNAL, id = 0) + val externalDisplay = display(TYPE_EXTERNAL, id = 1) + setDisplays(internalDisplay, externalDisplay) val onSaved = { _: Uri? -> } screenshotExecutor.executeScreenshots( createScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE), @@ -115,8 +120,8 @@ class TakeScreenshotExecutorTest : SysuiTestCase() { callback ) - verify(controllerFactory).create(eq(0), any()) - verify(controllerFactory, never()).create(eq(1), any()) + verify(controllerFactory).create(eq(internalDisplay), any()) + verify(controllerFactory, never()).create(eq(externalDisplay), any()) val capturer = ArgumentCaptor<ScreenshotData>() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index 127a3d7b208b..269510e0b4b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -168,6 +168,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { private FakePowerRepository mPowerRepository; @Mock private UserTracker mUserTracker; + @Mock + private HeadsUpManager mHeadsUpManager; private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); private ExpandableNotificationRow mNotificationRow; private ExpandableNotificationRow mBubbleNotificationRow; @@ -222,13 +224,12 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mScreenOffAnimationController, mStatusBarStateController).getPowerInteractor(); - HeadsUpManager headsUpManager = mock(HeadsUpManager.class); NotificationLaunchAnimatorControllerProvider notificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( new NotificationLaunchAnimationInteractor( new NotificationLaunchAnimationRepository()), mock(NotificationListContainer.class), - headsUpManager, + mHeadsUpManager, mJankMonitor); mNotificationActivityStarter = new StatusBarNotificationActivityStarter( @@ -237,7 +238,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mHandler, mUiBgExecutor, mVisibilityProvider, - headsUpManager, + mHeadsUpManager, mActivityStarter, mCommandQueue, mClickNotifier, @@ -417,6 +418,51 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { } @Test + public void testOnNotificationBubbleIconClicked_unbubble_keyGuardShowing() + throws RemoteException { + NotificationEntry entry = mBubbleNotificationRow.getEntry(); + StatusBarNotification sbn = entry.getSbn(); + + // Given + sbn.getNotification().contentIntent = mContentIntent; + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + + // When + mNotificationActivityStarter.onNotificationBubbleIconClicked(entry); + + // Then + verify(mBubblesManager).onUserChangedBubble(entry, false); + + verify(mHeadsUpManager).removeNotification(entry.getKey(), true); + + verifyNoMoreInteractions(mContentIntent); + verifyNoMoreInteractions(mShadeController); + } + + @Test + public void testOnNotificationBubbleIconClicked_bubble_keyGuardShowing() { + NotificationEntry entry = mNotificationRow.getEntry(); + StatusBarNotification sbn = entry.getSbn(); + + // Given + sbn.getNotification().contentIntent = mContentIntent; + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(true); + + // When + mNotificationActivityStarter.onNotificationBubbleIconClicked(entry); + + // Then + verify(mBubblesManager).onUserChangedBubble(entry, true); + + verify(mHeadsUpManager).removeNotification(entry.getKey(), true); + + verify(mContentIntent, atLeastOnce()).isActivity(); + verifyNoMoreInteractions(mContentIntent); + } + + @Test public void testOnFullScreenIntentWhenDozing_wakeUpDevice() { // GIVEN entry that can has a full screen intent that can show PendingIntent fullScreenIntent = PendingIntent.getActivity(mContext, 1, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt index 3a61bf625804..9b3482b8ce42 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt @@ -18,8 +18,12 @@ package com.android.systemui.common.data.repository import android.os.UserHandle import com.android.systemui.common.shared.model.PackageChangeModel +import com.android.systemui.common.shared.model.PackageInstallSession import com.android.systemui.util.time.SystemClock +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter class FakePackageChangeRepository(private val systemClock: SystemClock) : PackageChangeRepository { @@ -31,6 +35,15 @@ class FakePackageChangeRepository(private val systemClock: SystemClock) : Packag user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid) } + private val _packageInstallSessions = MutableStateFlow<List<PackageInstallSession>>(emptyList()) + + override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> = + _packageInstallSessions.asStateFlow() + + fun setInstallSessions(sessions: List<PackageInstallSession>) { + _packageInstallSessions.value = sessions + } + suspend fun notifyChange(model: PackageChangeModel) { _packageChanged.emit(model) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 329c0f1ab5b4..f7ce367ebb18 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -51,6 +51,7 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : override fun abortRestoreWidgets() {} private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { - _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) + _communalWidgets.value += + listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority)) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt new file mode 100644 index 000000000000..3190171b180d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt @@ -0,0 +1,28 @@ +/* + * 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.keyboard.shortcut + +import android.content.Intent + +class FakeShortcutHelperStartActivity : (Intent) -> Unit { + + val startIntents = mutableListOf<Intent>() + + override fun invoke(intent: Intent) { + startIntents += intent + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt new file mode 100644 index 000000000000..38f2a56d317f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.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.systemui.keyboard.shortcut + +import android.content.applicationContext +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper +import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor +import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter +import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel +import com.android.systemui.keyguard.data.repository.fakeCommandQueue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher + +val Kosmos.shortcutHelperRepository by + Kosmos.Fixture { ShortcutHelperRepository(fakeCommandQueue, broadcastDispatcher) } + +val Kosmos.shortcutHelperTestHelper by + Kosmos.Fixture { + ShortcutHelperTestHelper( + shortcutHelperRepository, + applicationContext, + broadcastDispatcher, + fakeCommandQueue + ) + } + +val Kosmos.shortcutHelperInteractor by + Kosmos.Fixture { ShortcutHelperInteractor(shortcutHelperRepository) } + +val Kosmos.shortcutHelperViewModel by + Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperInteractor) } + +val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() } + +val Kosmos.shortcutHelperActivityStarter by + Kosmos.Fixture { + ShortcutHelperActivityStarter( + applicationContext, + applicationCoroutineScope, + shortcutHelperViewModel, + fakeShortcutHelperStartActivity, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt new file mode 100644 index 000000000000..772ce415a6e9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt @@ -0,0 +1,56 @@ +/* + * 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.keyboard.shortcut.data.repository + +import android.content.Context +import android.content.Intent +import com.android.systemui.broadcast.FakeBroadcastDispatcher +import com.android.systemui.keyguard.data.repository.FakeCommandQueue + +class ShortcutHelperTestHelper( + repo: ShortcutHelperRepository, + private val context: Context, + private val fakeBroadcastDispatcher: FakeBroadcastDispatcher, + private val fakeCommandQueue: FakeCommandQueue, +) { + + init { + repo.start() + } + + fun hideFromActivity() { + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS) + ) + } + + fun showFromActivity() { + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS) + ) + } + + fun toggle(deviceId: Int) { + fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) } + } + + fun hideForSystem() { + fakeCommandQueue.doForEachCallback { it.dismissKeyboardShortcutsMenu() } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt index 302ac35f1a8a..093ebd6c6b63 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.pipeline.domain.interactor +package com.android.systemui.qs import com.android.internal.logging.InstanceId import com.android.systemui.animation.Expandable diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt new file mode 100644 index 000000000000..d686699f795b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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.qs.panels.data.repository + +import android.content.packageManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository +import com.android.systemui.settings.userTracker +import com.android.systemui.util.mockito.whenever + +val Kosmos.iconAndNameCustomRepository by + Kosmos.Fixture { + whenever(userTracker.userContext.packageManager).thenReturn(packageManager) + IconAndNameCustomRepository( + installedTilesRepository, + userTracker, + backgroundCoroutineContext, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt new file mode 100644 index 000000000000..ff33650d9f02 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.qs.panels.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase + +var Kosmos.stockTilesRepository by + Kosmos.Fixture { + testCase.context.orCreateTestableResources + StockTilesRepository(testCase.context.resources) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt new file mode 100644 index 000000000000..bd54fd471807 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.qs.panels.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository +import com.android.systemui.qs.panels.data.repository.stockTilesRepository +import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider + +val Kosmos.editTilesListInteractor by + Kosmos.Fixture { + EditTilesListInteractor( + stockTilesRepository, + qSTileConfigProvider, + iconAndNameCustomRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt new file mode 100644 index 000000000000..612a5d9917ff --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.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.systemui.qs.panels.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor +import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap +import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor +import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout +import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor +import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor + +val Kosmos.editModeViewModel by + Kosmos.Fixture { + EditModeViewModel( + editTilesListInteractor, + currentTilesInteractor, + minimumTilesInteractor, + infiniteGridLayout, + applicationCoroutineScope, + gridLayoutTypeInteractor, + gridLayoutMap, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt index ff6b7d083df7..ed4c67ee3511 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt @@ -17,23 +17,78 @@ package com.android.systemui.qs.pipeline.data.repository import android.content.ComponentName +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map class FakeInstalledTilesComponentRepository : InstalledTilesComponentRepository { - private val installedComponentsPerUser = - mutableMapOf<Int, MutableStateFlow<Set<ComponentName>>>() + private val installedServicesPerUser = mutableMapOf<Int, MutableStateFlow<List<ServiceInfo>>>() override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> { - return getFlow(userId).asStateFlow() + return getFlow(userId).map { it.map { it.componentName }.toSet() } + } + + override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> { + return getFlow(userId).value } fun setInstalledPackagesForUser(userId: Int, components: Set<ComponentName>) { - getFlow(userId).value = components + getFlow(userId).value = + components.map { + ServiceInfo().apply { + packageName = it.packageName + name = it.className + applicationInfo = ApplicationInfo() + } + } + } + + fun setInstalledServicesForUser(userId: Int, services: List<ServiceInfo>) { + getFlow(userId).value = services.toList() } - private fun getFlow(userId: Int): MutableStateFlow<Set<ComponentName>> = - installedComponentsPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) } + private fun getFlow(userId: Int): MutableStateFlow<List<ServiceInfo>> = + installedServicesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) } + + companion object { + fun ServiceInfo( + componentName: ComponentName, + serviceName: String, + serviceIcon: Drawable? = null, + appName: String = "", + appIcon: Drawable? = null + ): ServiceInfo { + val appInfo = + object : ApplicationInfo() { + override fun loadLabel(pm: PackageManager): CharSequence { + return appName + } + + override fun loadIcon(pm: PackageManager?): Drawable? { + return appIcon + } + } + val serviceInfo = + object : ServiceInfo() { + override fun loadLabel(pm: PackageManager): CharSequence { + return serviceName + } + + override fun loadIcon(pm: PackageManager?): Drawable? { + return serviceIcon ?: getApplicationInfo().loadIcon(pm) + } + } + .apply { + packageName = componentName.packageName + name = componentName.className + applicationInfo = appInfo + } + return serviceInfo + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt new file mode 100644 index 000000000000..ef1189f25cff --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.qs.pipeline.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository + +var Kosmos.minimumTilesInteractor by + Kosmos.Fixture { MinimumTilesInteractor(minimumTilesRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 3e9ae4d2e354..1f2ecb7d172d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.yield class FakeUserRepository @Inject constructor() : UserRepository { companion object { // User id to represent a non system (human) user id. We presume this is the main user. - private const val MAIN_USER_ID = 10 + const val MAIN_USER_ID = 10 private const val DEFAULT_SELECTED_USER = 0 private val DEFAULT_SELECTED_USER_INFO = @@ -84,6 +84,10 @@ class FakeUserRepository @Inject constructor() : UserRepository { override var isRefreshUsersPaused: Boolean = false + override suspend fun getMainUserId(): Int? { + return MAIN_USER_ID + } + var refreshUsersCallCount: Int = 0 private set diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index 2f54f8c8bdb5..2a7458fca249 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -1469,10 +1469,7 @@ public class TouchExplorer extends BaseEventStreamTransformation int policyFlags = mState.getLastReceivedPolicyFlags(); if (mState.isDragging()) { // Send an event to the end of the drag gesture. - int pointerIdBits = ALL_POINTER_ID_BITS; - if (Flags.fixDragPointerWhenEndingDrag()) { - pointerIdBits = 1 << mDraggingPointerId; - } + int pointerIdBits = 1 << mDraggingPointerId; mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags); } mState.startDelegating(); diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 9b72288955a7..d7c65c748d8f 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -41,7 +41,6 @@ import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; -import android.view.Display; import android.view.InputDevice; import android.view.WindowManager; @@ -169,7 +168,6 @@ class InputController { createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId, deviceToken, displayId, phys, () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys)); - setVirtualMousePointerDisplayId(displayId); } void createTouchscreen(@NonNull String deviceName, int vendorId, int productId, @@ -236,15 +234,6 @@ class InputController { if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) { mInputManagerInternal.removeKeyboardLayoutAssociation(phys); } - - // Reset values to the default if all virtual mice are unregistered, or set display - // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be - // removed from the mInputDeviceDescriptors instance variable prior to this point. - if (inputDeviceDescriptor.isMouse()) { - if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) { - updateActivePointerDisplayIdLocked(); - } - } } /** @@ -276,29 +265,6 @@ class InputController { mWindowManager.setDisplayImePolicy(displayId, policy); } - // TODO(b/293587049): Remove after pointer icon refactor is complete. - @GuardedBy("mLock") - private void updateActivePointerDisplayIdLocked() { - InputDeviceDescriptor mostRecentlyCreatedMouse = null; - for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) { - InputDeviceDescriptor otherInputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i); - if (otherInputDeviceDescriptor.isMouse()) { - if (mostRecentlyCreatedMouse == null - || (otherInputDeviceDescriptor.getCreationOrderNumber() - > mostRecentlyCreatedMouse.getCreationOrderNumber())) { - mostRecentlyCreatedMouse = otherInputDeviceDescriptor; - } - } - } - if (mostRecentlyCreatedMouse != null) { - setVirtualMousePointerDisplayId( - mostRecentlyCreatedMouse.getDisplayId()); - } else { - // All mice have been unregistered - setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY); - } - } - /** * Validates a device name by checking whether a device with the same name already exists. * @param deviceName The name of the device to be validated @@ -355,9 +321,6 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { - setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); - } return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(), event.getButtonCode(), event.getAction(), event.getEventTimeNanos()); } @@ -384,9 +347,6 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { - setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); - } return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(), event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos()); } @@ -399,9 +359,6 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { - setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); - } return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(), event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos()); } @@ -415,9 +372,6 @@ class InputController { throw new IllegalArgumentException( "Could not get cursor position for input device for given token"); } - if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { - setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); - } return LocalServices.getService(InputManagerInternal.class).getCursorPosition( inputDeviceDescriptor.getDisplayId()); } @@ -878,22 +832,4 @@ class InputController { /** Returns true if the calling thread is a valid thread for device creation. */ boolean isValidThread(); } - - // TODO(b/293587049): Remove after pointer icon refactor is complete. - private void setVirtualMousePointerDisplayId(int displayId) { - if (com.android.input.flags.Flags.enablePointerChoreographer()) { - // We no longer need to set the pointer display when pointer choreographer is enabled. - return; - } - mInputManagerInternal.setVirtualMousePointerDisplayId(displayId); - } - - // TODO(b/293587049): Remove after pointer icon refactor is complete. - private int getVirtualMousePointerDisplayId() { - if (com.android.input.flags.Flags.enablePointerChoreographer()) { - // We no longer need to get the pointer display when pointer choreographer is enabled. - return Display.INVALID_DISPLAY; - } - return mInputManagerInternal.getVirtualMousePointerDisplayId(); - } } diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 7acca19f9d79..e2b6bd6ae360 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -36,6 +36,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.AppOpsManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -47,6 +48,7 @@ import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.net.vcn.Flags; import android.net.vcn.IVcnManagementService; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; @@ -68,6 +70,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.os.UserHandle; +import android.os.UserManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -431,6 +434,8 @@ public class VcnManagementService extends IVcnManagementService.Stub { mTelephonySubscriptionTracker.register(); } + // The system server automatically has the required permissions for #getMainUser() + @SuppressLint("AndroidFrameworkRequiresPermission") private void enforcePrimaryUser() { final int uid = mDeps.getBinderCallingUid(); if (uid == Process.SYSTEM_UID) { @@ -438,7 +443,20 @@ public class VcnManagementService extends IVcnManagementService.Stub { "Calling identity was System Server. Was Binder calling identity cleared?"); } - if (!UserHandle.getUserHandleForUid(uid).isSystem()) { + final UserHandle userHandle = UserHandle.getUserHandleForUid(uid); + + if (Flags.enforceMainUser()) { + final UserManager userManager = mContext.getSystemService(UserManager.class); + + Binder.withCleanCallingIdentity( + () -> { + if (!Objects.equals(userManager.getMainUser(), userHandle)) { + throw new SecurityException( + "VcnManagementService can only be used by callers running as" + + " the main user"); + } + }); + } else if (!userHandle.isSystem()) { throw new SecurityException( "VcnManagementService can only be used by callers running as the primary user"); } diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 3cea01464c10..a182a106ed8c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -115,6 +115,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.DisplayMetrics; +import android.util.SparseArray; import android.util.TeeWriter; import android.util.proto.ProtoOutputStream; import android.view.Choreographer; @@ -275,9 +276,9 @@ final class ActivityManagerShellCommand extends ShellCommand { case "compact": return runCompact(pw); case "freeze": - return runFreeze(pw); + return runFreeze(pw, true); case "unfreeze": - return runUnfreeze(pw); + return runFreeze(pw, false); case "instrument": getOutPrintWriter().println("Error: must be invoked through 'am instrument'."); return -1; @@ -1203,45 +1204,27 @@ final class ActivityManagerShellCommand extends ShellCommand { } @NeverCompile - int runFreeze(PrintWriter pw) throws RemoteException { + int runFreeze(PrintWriter pw, boolean freeze) throws RemoteException { String freezerOpt = getNextOption(); boolean isSticky = false; - if (freezerOpt != null) { - isSticky = freezerOpt.equals("--sticky"); - } - ProcessRecord app = getProcessFromShell(); - if (app == null) { - getErrPrintWriter().println("Error: could not find process"); - return -1; - } - pw.println("Freezing pid: " + app.mPid + " sticky=" + isSticky); - synchronized (mInternal) { - synchronized (mInternal.mProcLock) { - app.mOptRecord.setFreezeSticky(isSticky); - mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app); - } - } - return 0; - } - @NeverCompile - int runUnfreeze(PrintWriter pw) throws RemoteException { - String freezerOpt = getNextOption(); - boolean isSticky = false; if (freezerOpt != null) { isSticky = freezerOpt.equals("--sticky"); } - ProcessRecord app = getProcessFromShell(); - if (app == null) { - getErrPrintWriter().println("Error: could not find process"); + ProcessRecord proc = getProcessFromShell(); + if (proc == null) { return -1; } - pw.println("Unfreezing pid: " + app.mPid); + pw.print(freeze ? "Freezing" : "Unfreezing"); + pw.print(" process " + proc.processName); + pw.println(" (" + proc.mPid + ") sticky=" + isSticky); synchronized (mInternal) { synchronized (mInternal.mProcLock) { - synchronized (mInternal.mOomAdjuster.mCachedAppOptimizer.mFreezerLock) { - app.mOptRecord.setFreezeSticky(isSticky); - mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(app, 0, + proc.mOptRecord.setFreezeSticky(isSticky); + if (freeze) { + mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(proc); + } else { + mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(proc, 0, true); } } @@ -1250,43 +1233,42 @@ final class ActivityManagerShellCommand extends ShellCommand { } /** - * Parses from the shell the process name and user id if provided and provides the corresponding - * {@link ProcessRecord)} If no user is provided, it will fallback to current user. - * Example usage: {@code <processname> --user current} or {@code <processname>} - * @return process record of process, null if none found. + * Parses from the shell the pid or process name and provides the corresponding + * {@link ProcessRecord}. + * Example usage: {@code <processname>} or {@code <pid>} + * @return process record of process, null if none or more than one found. * @throws RemoteException */ @NeverCompile ProcessRecord getProcessFromShell() throws RemoteException { - ProcessRecord app; - String processName = getNextArgRequired(); - synchronized (mInternal.mProcLock) { - // Default to current user - int userId = getUserIdFromShellOrFallback(); - final int uid = - mInternal.getPackageManagerInternal().getPackageUid(processName, 0, userId); - app = mInternal.getProcessRecordLocked(processName, uid); - } - return app; - } + ProcessRecord proc = null; + String process = getNextArgRequired(); + try { + int pid = Integer.parseInt(process); + synchronized (mInternal.mPidsSelfLocked) { + proc = mInternal.mPidsSelfLocked.get(pid); + } + } catch (NumberFormatException e) { + // Fallback to process name if it's not a valid pid + } - /** - * @return User id from command line provided in the form of - * {@code --user <userid|current|all>} and if the argument is not found it will fallback - * to current user. - * @throws RemoteException - */ - @NeverCompile - int getUserIdFromShellOrFallback() throws RemoteException { - int userId = mInterface.getCurrentUserId(); - String userOpt = getNextOption(); - if (userOpt != null && "--user".equals(userOpt)) { - int inputUserId = UserHandle.parseUserArg(getNextArgRequired()); - if (inputUserId != UserHandle.USER_CURRENT) { - userId = inputUserId; + if (proc == null) { + synchronized (mInternal.mProcLock) { + ArrayMap<String, SparseArray<ProcessRecord>> all = + mInternal.mProcessList.getProcessNamesLOSP().getMap(); + SparseArray<ProcessRecord> procs = all.get(process); + if (procs == null || procs.size() == 0) { + getErrPrintWriter().println("Error: could not find process"); + return null; + } else if (procs.size() > 1) { + getErrPrintWriter().println("Error: more than one processes found"); + return null; + } + proc = procs.valueAt(0); } } - return userId; + + return proc; } int runDumpHeap(PrintWriter pw) throws RemoteException { @@ -4306,24 +4288,26 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" --allow-background-activity-starts: The receiver may start activities"); pw.println(" even if in the background."); pw.println(" --async: Send without waiting for the completion of the receiver."); - pw.println(" compact [some|full] <process_name> [--user <USER_ID>]"); - pw.println(" Perform a single process compaction."); + pw.println(" compact {some|full} <PROCESS>"); + pw.println(" Perform a single process compaction. The given <PROCESS> argument"); + pw.println(" may be either a process name or pid."); pw.println(" some: execute file compaction."); pw.println(" full: execute anon + file compaction."); - pw.println(" system: system compaction."); pw.println(" compact system"); pw.println(" Perform a full system compaction."); - pw.println(" compact native [some|full] <pid>"); + pw.println(" compact native {some|full} <pid>"); pw.println(" Perform a native compaction for process with <pid>."); pw.println(" some: execute file compaction."); pw.println(" full: execute anon + file compaction."); - pw.println(" freeze [--sticky] <processname> [--user <USER_ID>]"); - pw.println(" Freeze a process."); - pw.println(" --sticky: persists the frozen state for the process lifetime or"); + pw.println(" freeze [--sticky] <PROCESS>"); + pw.println(" Freeze a process. The given <PROCESS> argument"); + pw.println(" may be either a process name or pid. Options are:"); + pw.println(" --sticky: persists the frozen state for the process lifetime or"); pw.println(" until an unfreeze is triggered via shell"); - pw.println(" unfreeze [--sticky] <processname> [--user <USER_ID>]"); - pw.println(" Unfreeze a process."); - pw.println(" --sticky: persists the unfrozen state for the process lifetime or"); + pw.println(" unfreeze [--sticky] <PROCESS>"); + pw.println(" Unfreeze a process. The given <PROCESS> argument"); + pw.println(" may be either a process name or pid. Options are:"); + pw.println(" --sticky: persists the unfrozen state for the process lifetime or"); pw.println(" until a freeze is triggered via shell"); pw.println(" instrument [-r] [-e <NAME> <VALUE>] [-p <FILE>] [-w]"); pw.println(" [--user <USER_ID> | current]"); diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 95206212c99d..a25817d491c4 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -185,6 +185,7 @@ public class SettingsToPropertiesMapper { "pmw", "power", "preload_safety", + "printing", "privacy_infra_policy", "resource_manager", "responsible_apis", diff --git a/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java new file mode 100644 index 000000000000..3e8acee26e81 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.biometrics; + +import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS; + +import android.annotation.NonNull; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.provider.Settings; +import android.util.Slog; + +import com.android.server.biometrics.sensors.BiometricNotificationUtils; + +/** + * Receives broadcast to biometrics dangling notification. + */ +public class BiometricDanglingReceiver extends BroadcastReceiver { + private static final String TAG = "BiometricDanglingReceiver"; + + public static final String ACTION_FINGERPRINT_RE_ENROLL_LAUNCH = + "action_fingerprint_re_enroll_launch"; + public static final String ACTION_FINGERPRINT_RE_ENROLL_DISMISS = + "action_fingerprint_re_enroll_dismiss"; + + public static final String ACTION_FACE_RE_ENROLL_LAUNCH = + "action_face_re_enroll_launch"; + public static final String ACTION_FACE_RE_ENROLL_DISMISS = + "action_face_re_enroll_dismiss"; + + public static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS"; + + private static final String SETTINGS_PACKAGE = "com.android.settings"; + + /** + * Constructor for BiometricDanglingReceiver. + * + * @param context context + * @param modality the value from BiometricsProtoEnums.MODALITY_* + */ + public BiometricDanglingReceiver(@NonNull Context context, int modality) { + final IntentFilter intentFilter = new IntentFilter(); + if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) { + intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_LAUNCH); + intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_DISMISS); + } else if (modality == BiometricsProtoEnums.MODALITY_FACE) { + intentFilter.addAction(ACTION_FACE_RE_ENROLL_LAUNCH); + intentFilter.addAction(ACTION_FACE_RE_ENROLL_DISMISS); + } + context.registerReceiver(this, intentFilter); + } + + @Override + public void onReceive(Context context, Intent intent) { + Slog.d(TAG, "Received: " + intent.getAction()); + if (ACTION_FINGERPRINT_RE_ENROLL_LAUNCH.equals(intent.getAction())) { + launchBiometricEnrollActivity(context, Settings.ACTION_FINGERPRINT_ENROLL); + BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context); + } else if (ACTION_FINGERPRINT_RE_ENROLL_DISMISS.equals(intent.getAction())) { + BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context); + } else if (ACTION_FACE_RE_ENROLL_LAUNCH.equals(intent.getAction())) { + launchBiometricEnrollActivity(context, FACE_SETTINGS_ACTION); + BiometricNotificationUtils.cancelFaceReEnrollNotification(context); + } else if (ACTION_FACE_RE_ENROLL_DISMISS.equals(intent.getAction())) { + BiometricNotificationUtils.cancelFaceReEnrollNotification(context); + } + context.unregisterReceiver(this); + } + + private void launchBiometricEnrollActivity(Context context, String action) { + context.sendBroadcast(new Intent(ACTION_CLOSE_SYSTEM_DIALOGS)); + final Intent intent = new Intent(action); + intent.setPackage(SETTINGS_PACKAGE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java index 0e22f7511af9..eaa5e2a6befb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java @@ -24,13 +24,18 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.face.FaceEnrollOptions; import android.hardware.fingerprint.FingerprintEnrollOptions; import android.os.SystemClock; import android.os.UserHandle; +import android.text.BidiFormatter; import android.util.Slog; import com.android.internal.R; +import com.android.server.biometrics.BiometricDanglingReceiver; + +import java.util.List; /** * Biometric notification helper class. @@ -39,6 +44,7 @@ public class BiometricNotificationUtils { private static final String TAG = "BiometricNotificationUtils"; private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll"; + private static final String FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG = "FingerprintReEnroll"; private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration"; private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock"; private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS"; @@ -50,6 +56,8 @@ public class BiometricNotificationUtils { private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel"; private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel"; private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel"; + private static final String FINGERPRINT_RE_ENROLL_CHANNEL = + "FingerprintReEnrollNotificationChannel"; private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL = "FingerprintBadCalibrationNotificationChannel"; private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000; @@ -177,10 +185,124 @@ public class BiometricNotificationUtils { BAD_CALIBRATION_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false); } + /** + * Shows a biometric re-enroll notification. + */ + public static void showBiometricReEnrollNotification(@NonNull Context context, + @NonNull List<String> identifiers, boolean allIdentifiersDeleted, int modality) { + final boolean isFingerprint = modality == BiometricsProtoEnums.MODALITY_FINGERPRINT; + final String reEnrollName = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG + : FACE_RE_ENROLL_NOTIFICATION_TAG; + if (identifiers.isEmpty()) { + Slog.v(TAG, "Skipping " + reEnrollName + " notification : empty list"); + return; + } + Slog.d(TAG, "Showing " + reEnrollName + " notification :[" + identifiers.size() + + " identifier(s) deleted, allIdentifiersDeleted=" + allIdentifiersDeleted + "]"); + + final String name = + context.getString(R.string.device_unlock_notification_name); + final String title = context.getString(isFingerprint + ? R.string.fingerprint_dangling_notification_title + : R.string.face_dangling_notification_title); + final String content = isFingerprint + ? getFingerprintDanglingContentString(context, identifiers, allIdentifiersDeleted) + : context.getString(R.string.face_dangling_notification_msg); + + // Create "Set up" notification action button. + final Intent setupIntent = new Intent( + isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH + : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH); + final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0, + setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT); + final String setupText = + context.getString(R.string.biometric_dangling_notification_action_set_up); + final Notification.Action setupAction = new Notification.Action.Builder( + null, setupText, setupPendingIntent).build(); + + // Create "Not now" notification action button. + final Intent notNowIntent = new Intent( + isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS + : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_DISMISS); + final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0, + notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT); + final String notNowText = context.getString( + R.string.biometric_dangling_notification_action_not_now); + final Notification.Action notNowAction = new Notification.Action.Builder( + null, notNowText, notNowPendingIntent).build(); + + final String channel = isFingerprint ? FINGERPRINT_RE_ENROLL_CHANNEL + : FACE_RE_ENROLL_CHANNEL; + final String tag = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG + : FACE_RE_ENROLL_NOTIFICATION_TAG; + + showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction, + notNowAction, Notification.CATEGORY_SYSTEM, channel, tag, + Notification.VISIBILITY_SECRET, false); + } + + private static String getFingerprintDanglingContentString(Context context, + @NonNull List<String> fingerprints, boolean allFingerprintDeleted) { + if (fingerprints.isEmpty()) { + return null; + } + + final int resId; + final int size = fingerprints.size(); + final StringBuilder first = new StringBuilder(); + final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); + if (size > 1) { + // If there are more than 1 fingerprint deleted, the "second" will be the last + // fingerprint and set the others to "first". + // For example, if we have 3 fingerprints deleted(fp1, fp2 and fp3): + // first = "fp1, fp2" + // second = "fp3" + final String separator = ", "; + String second = null; + for (int i = 0; i < size; i++) { + if (i == size - 1) { + second = bidiFormatter.unicodeWrap("\"" + fingerprints.get(i) + "\""); + } else { + first.append(bidiFormatter.unicodeWrap("\"")); + first.append(bidiFormatter.unicodeWrap(fingerprints.get(i))); + first.append(bidiFormatter.unicodeWrap("\"")); + if (i < size - 2) { + first.append(bidiFormatter.unicodeWrap(separator)); + } + } + } + if (allFingerprintDeleted) { + resId = R.string.fingerprint_dangling_notification_msg_all_deleted_2; + } else { + resId = R.string.fingerprint_dangling_notification_msg_2; + } + + return String.format(context.getString(resId), first, second); + } else { + if (allFingerprintDeleted) { + resId = R.string.fingerprint_dangling_notification_msg_all_deleted_1; + } else { + resId = R.string.fingerprint_dangling_notification_msg_1; + } + first.append(bidiFormatter.unicodeWrap("\"")); + first.append(bidiFormatter.unicodeWrap(fingerprints.get(0))); + first.append(bidiFormatter.unicodeWrap("\"")); + return String.format(context.getString(resId), first); + } + } + + private static void showNotificationHelper(Context context, String name, String title, + String content, PendingIntent pendingIntent, String category, String channelName, + String notificationTag, int visibility, boolean listenToDismissEvent) { + showNotificationHelper(context, name, title, content, pendingIntent, + null /* positiveAction */, null /* negativeAction */, category, channelName, + notificationTag, visibility, listenToDismissEvent); + } + private static void showNotificationHelper(Context context, String name, String title, - String content, PendingIntent pendingIntent, String category, - String channelName, String notificationTag, int visibility, - boolean listenToDismissEvent) { + String content, PendingIntent pendingIntent, Notification.Action positiveAction, + Notification.Action negativeAction, String category, String channelName, + String notificationTag, int visibility, boolean listenToDismissEvent) { Slog.v(TAG," listenToDismissEvent = " + listenToDismissEvent); final PendingIntent dismissIntent = PendingIntent.getActivityAsUser(context, 0 /* requestCode */, DISMISS_FRR_INTENT, PendingIntent.FLAG_IMMUTABLE /* flags */, @@ -202,6 +324,12 @@ public class BiometricNotificationUtils { .setContentIntent(pendingIntent) .setVisibility(visibility); + if (positiveAction != null) { + builder.addAction(positiveAction); + } + if (negativeAction != null) { + builder.addAction(negativeAction); + } if (listenToDismissEvent) { builder.setDeleteIntent(dismissIntent); } @@ -253,4 +381,14 @@ public class BiometricNotificationUtils { UserHandle.CURRENT); } + /** + * Cancels a fingerprint enrollment notification + */ + public static void cancelFingerprintReEnrollNotification(@NonNull Context context) { + final NotificationManager notificationManager = + context.getSystemService(NotificationManager.class); + notificationManager.cancelAsUser(FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID, + UserHandle.CURRENT); + } + } diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java index 6daaad1baf83..81ab26dd581e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java @@ -22,6 +22,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.os.IBinder; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.BiometricsProto; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -44,6 +45,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> private List<? extends BiometricAuthenticator.Identifier> mEnrolledList; // List of templates to remove from the HAL private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>(); + private final int mInitialEnrolledSize; protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon, @NonNull IBinder token, int userId, @NonNull String owner, @@ -55,6 +57,7 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner, 0 /* cookie */, sensorId, logger, biometricContext); mEnrolledList = enrolledList; + mInitialEnrolledSize = mEnrolledList.size(); mUtils = utils; } @@ -111,8 +114,10 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> // At this point, mEnrolledList only contains templates known to the framework and // not the HAL. + final List<String> names = new ArrayList<>(); for (int i = 0; i < mEnrolledList.size(); i++) { BiometricAuthenticator.Identifier identifier = mEnrolledList.get(i); + names.add(identifier.getName().toString()); Slog.e(TAG, "doTemplateCleanup(): Removing dangling template from framework: " + identifier.getBiometricId() + " " + identifier.getName()); mUtils.removeBiometricForUser(getContext(), @@ -120,6 +125,11 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> getLogger().logUnknownEnrollmentInFramework(); } + + // Send dangling notification. + if (!names.isEmpty()) { + sendDanglingNotification(names); + } mEnrolledList.clear(); } @@ -127,8 +137,24 @@ public abstract class InternalEnumerateClient<T> extends HalClientMonitor<T> return mUnknownHALTemplates; } + /** + * Send the dangling notification. + */ + @VisibleForTesting + public void sendDanglingNotification(@NonNull List<String> identifierNames) { + if (!identifierNames.isEmpty()) { + Slog.e(TAG, "sendDanglingNotification(): initial enrolledSize=" + + mInitialEnrolledSize + ", after clean up size=" + mEnrolledList.size()); + final boolean allIdentifiersDeleted = mEnrolledList.size() == mInitialEnrolledSize; + BiometricNotificationUtils.showBiometricReEnrollNotification( + getContext(), identifierNames, allIdentifiersDeleted, getModality()); + } + } + @Override public int getProtoEnum() { return BiometricsProto.CM_ENUMERATE; } + + protected abstract int getModality(); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java index d85455e0e76b..6ce3bc59eb56 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java @@ -18,12 +18,14 @@ package com.android.server.biometrics.sensors.face.aidl; import android.annotation.NonNull; import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.face.IFace; import android.hardware.face.Face; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.BiometricUtils; @@ -35,7 +37,8 @@ import java.util.function.Supplier; /** * Face-specific internal enumerate client for the {@link IFace} AIDL HAL interface. */ -class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> { +@VisibleForTesting +public class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> { private static final String TAG = "FaceInternalEnumerateClient"; FaceInternalEnumerateClient(@NonNull Context context, @@ -56,4 +59,9 @@ class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> { mCallback.onClientFinished(this, false /* success */); } } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FACE; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index e71cffe0b8c5..f0a418951505 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -52,6 +52,7 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; import com.android.server.biometrics.AuthenticationStatsCollector; +import com.android.server.biometrics.BiometricDanglingReceiver; import com.android.server.biometrics.BiometricHandlerProvider; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; @@ -201,6 +202,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mBiometricHandlerProvider = biometricHandlerProvider; initAuthenticationBroadcastReceiver(); + initFaceDanglingBroadcastReceiver(); initSensors(resetLockoutRequiresChallenge, props); } @@ -214,6 +216,10 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { }); } + private void initFaceDanglingBroadcastReceiver() { + new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FACE); + } + private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) { if (resetLockoutRequiresChallenge) { Slog.d(getTag(), "Adding HIDL configs"); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java index a5a832aaaf59..2849bd9c92a3 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java @@ -18,11 +18,13 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; import android.annotation.NonNull; import android.content.Context; +import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.fingerprint.Fingerprint; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.BiometricUtils; @@ -35,7 +37,8 @@ import java.util.function.Supplier; * Fingerprint-specific internal client supporting the * {@link android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface. */ -class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> { +@VisibleForTesting +public class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> { private static final String TAG = "FingerprintInternalEnumerateClient"; protected FingerprintInternalEnumerateClient(@NonNull Context context, @@ -56,4 +59,9 @@ class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSes mCallback.onClientFinished(this, false /* success */); } } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FINGERPRINT; + } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 6874c71267fb..c0dcd4943260 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -58,6 +58,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver; import com.android.server.biometrics.AuthenticationStatsCollector; +import com.android.server.biometrics.BiometricDanglingReceiver; import com.android.server.biometrics.BiometricHandlerProvider; import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; @@ -205,6 +206,7 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mBiometricHandlerProvider = biometricHandlerProvider; initAuthenticationBroadcastReceiver(); + initFingerprintDanglingBroadcastReceiver(); initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher); } @@ -218,6 +220,10 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi }); } + private void initFingerprintDanglingBroadcastReceiver() { + new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT); + } + private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props, GestureAvailabilityDispatcher gestureAvailabilityDispatcher) { if (!resetLockoutRequiresHardwareAuthToken) { diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 4e9cf51fda56..b47631c35e38 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -84,29 +84,6 @@ public abstract class InputManagerInternal { @NonNull IBinder toChannelToken); /** - * Sets the display id that the MouseCursorController will be forced to target. Pass - * {@link android.view.Display#INVALID_DISPLAY} to clear the override. - * - * Note: This method generally blocks until the pointer display override has propagated. - * When setting a new override, the caller should ensure that an input device that can control - * the mouse pointer is connected. If a new override is set when no such input device is - * connected, the caller may be blocked for an arbitrary period of time. - * - * @return true if the pointer displayId was set successfully, or false if it fails. - * - * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. - */ - public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId); - - /** - * Gets the display id that the MouseCursorController is being forced to target. Returns - * {@link android.view.Display#INVALID_DISPLAY} if there is no override - * - * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. - */ - public abstract int getVirtualMousePointerDisplayId(); - - /** * Gets the current position of the mouse cursor. * * Returns NaN-s as the coordinates if the cursor is not available. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index eb719527b06d..cbd309e1f957 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -282,33 +282,9 @@ public class InputManagerService extends IInputManager.Stub // WARNING: Do not call other services outside of input while holding this lock. private final Object mAdditionalDisplayInputPropertiesLock = new Object(); - // Forces the PointerController to target a specific display id. - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private int mOverriddenPointerDisplayId = Display.INVALID_DISPLAY; - - // PointerController is the source of truth of the pointer display. This is the value of the - // latest pointer display id reported by PointerController. - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private int mAcknowledgedPointerDisplayId = Display.INVALID_DISPLAY; - // This is the latest display id that IMS has requested PointerController to use. If there are - // no devices that can control the pointer, PointerController may end up disregarding this - // value. - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private int mRequestedPointerDisplayId = Display.INVALID_DISPLAY; @GuardedBy("mAdditionalDisplayInputPropertiesLock") private final SparseArray<AdditionalDisplayInputProperties> mAdditionalDisplayInputProperties = new SparseArray<>(); - // This contains the per-display properties that are currently applied by native code. It should - // be kept in sync with the properties for mRequestedPointerDisplayId. - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private final AdditionalDisplayInputProperties mCurrentDisplayProperties = - new AdditionalDisplayInputProperties(); - // TODO(b/293587049): Pointer Icon Refactor: There can be more than one pointer icon - // visible at once. Update this to support multi-pointer use cases. - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED; - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private PointerIcon mPointerIcon; // Holds all the registered gesture monitors that are implemented as spy windows. The spy // windows are mapped by their InputChannel tokens. @@ -617,14 +593,9 @@ public class InputManagerService extends IInputManager.Stub } mNative.setDisplayViewports(vArray); - // Attempt to update the pointer display when viewports change when there is no override. + // Attempt to update the default pointer display when the viewports change. // Take care to not make calls to window manager while holding internal locks. - final int pointerDisplayId = mWindowManagerCallbacks.getPointerDisplayId(); - synchronized (mAdditionalDisplayInputPropertiesLock) { - if (mOverriddenPointerDisplayId == Display.INVALID_DISPLAY) { - updatePointerDisplayIdLocked(pointerDisplayId); - } - } + mNative.setPointerDisplayId(mWindowManagerCallbacks.getPointerDisplayId()); } /** @@ -1353,84 +1324,11 @@ public class InputManagerService extends IInputManager.Stub properties -> properties.pointerIconVisible = visible); } - /** - * Update the display on which the mouse pointer is shown. - * - * @return true if the pointer displayId changed, false otherwise. - */ - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private boolean updatePointerDisplayIdLocked(int pointerDisplayId) { - if (mRequestedPointerDisplayId == pointerDisplayId) { - return false; - } - mRequestedPointerDisplayId = pointerDisplayId; - mNative.setPointerDisplayId(pointerDisplayId); - applyAdditionalDisplayInputProperties(); - return true; - } - private void handlePointerDisplayIdChanged(PointerDisplayIdChangedArgs args) { - synchronized (mAdditionalDisplayInputPropertiesLock) { - mAcknowledgedPointerDisplayId = args.mPointerDisplayId; - // Notify waiting threads that the display of the mouse pointer has changed. - mAdditionalDisplayInputPropertiesLock.notifyAll(); - } mWindowManagerCallbacks.notifyPointerDisplayIdChanged( args.mPointerDisplayId, args.mXPosition, args.mYPosition); } - private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) { - if (com.android.input.flags.Flags.enablePointerChoreographer()) { - throw new IllegalStateException( - "This must not be used when PointerChoreographer is enabled"); - } - final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY; - - // Take care to not make calls to window manager while holding internal locks. - final int resolvedDisplayId = isRemovingOverride - ? mWindowManagerCallbacks.getPointerDisplayId() - : overrideDisplayId; - - synchronized (mAdditionalDisplayInputPropertiesLock) { - mOverriddenPointerDisplayId = overrideDisplayId; - - if (!updatePointerDisplayIdLocked(resolvedDisplayId) - && mAcknowledgedPointerDisplayId == resolvedDisplayId) { - // The requested pointer display is already set. - return true; - } - if (isRemovingOverride && mAcknowledgedPointerDisplayId == Display.INVALID_DISPLAY) { - // The pointer display override is being removed, but the current pointer display - // is already invalid. This can happen when the PointerController is destroyed as a - // result of the removal of all input devices that can control the pointer. - return true; - } - try { - // The pointer display changed, so wait until the change has propagated. - mAdditionalDisplayInputPropertiesLock.wait(5_000 /*mills*/); - } catch (InterruptedException ignored) { - } - // This request succeeds in two cases: - // - This request was to remove the override, in which case the new pointer display - // could be anything that WM has set. - // - We are setting a new override, in which case the request only succeeds if the - // reported new displayId is the one we requested. This check ensures that if two - // competing overrides are requested in succession, the caller can be notified if one - // of them fails. - return isRemovingOverride || mAcknowledgedPointerDisplayId == overrideDisplayId; - } - } - - private int getVirtualMousePointerDisplayId() { - if (com.android.input.flags.Flags.enablePointerChoreographer()) { - throw new IllegalStateException( - "This must not be used when PointerChoreographer is enabled"); - } - synchronized (mAdditionalDisplayInputPropertiesLock) { - return mOverriddenPointerDisplayId; - } - } - private void setDisplayEligibilityForPointerCapture(int displayId, boolean isEligible) { mNative.setDisplayEligibilityForPointerCapture(displayId, isEligible); } @@ -1715,31 +1613,13 @@ public class InputManagerService extends IInputManager.Stub // Binder call @Override public void setPointerIconType(int iconType) { - if (iconType == PointerIcon.TYPE_CUSTOM) { - throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers"); - } - synchronized (mAdditionalDisplayInputPropertiesLock) { - mPointerIcon = null; - mPointerIconType = iconType; - - if (!mCurrentDisplayProperties.pointerIconVisible) return; - - mNative.setPointerIconType(mPointerIconType); - } + // TODO(b/311416205): Remove. } // Binder call @Override public void setCustomPointerIcon(PointerIcon icon) { - Objects.requireNonNull(icon); - synchronized (mAdditionalDisplayInputPropertiesLock) { - mPointerIconType = PointerIcon.TYPE_CUSTOM; - mPointerIcon = icon; - - if (!mCurrentDisplayProperties.pointerIconVisible) return; - - mNative.setCustomPointerIcon(mPointerIcon); - } + // TODO(b/311416205): Remove. } // Binder call @@ -1747,12 +1627,7 @@ public class InputManagerService extends IInputManager.Stub public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId, IBinder inputToken) { Objects.requireNonNull(icon); - synchronized (mAdditionalDisplayInputPropertiesLock) { - mPointerIconType = icon.getType(); - mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null; - - return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken); - } + return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken); } /** @@ -2281,28 +2156,24 @@ public class InputManagerService extends IInputManager.Stub private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) { synchronized (mAdditionalDisplayInputPropertiesLock) { - if (mAdditionalDisplayInputProperties.size() != 0) { - pw.println("mAdditionalDisplayInputProperties:"); - pw.increaseIndent(); + pw.println("mAdditionalDisplayInputProperties:"); + pw.increaseIndent(); + try { + if (mAdditionalDisplayInputProperties.size() == 0) { + pw.println("<none>"); + return; + } for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) { - pw.println("displayId: " - + mAdditionalDisplayInputProperties.keyAt(i)); + pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i)); final AdditionalDisplayInputProperties properties = mAdditionalDisplayInputProperties.valueAt(i); pw.println("mousePointerAccelerationEnabled: " + properties.mousePointerAccelerationEnabled); pw.println("pointerIconVisible: " + properties.pointerIconVisible); } + } finally { pw.decreaseIndent(); } - if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) { - pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId); - } - - pw.println("mAcknowledgedPointerDisplayId=" + mAcknowledgedPointerDisplayId); - pw.println("mRequestedPointerDisplayId=" + mRequestedPointerDisplayId); - pw.println("mPointerIconType=" + PointerIcon.typeToString(mPointerIconType)); - pw.println("mPointerIcon=" + mPointerIcon); } } private boolean checkCallingPermission(String permission, String func) { @@ -3267,17 +3138,6 @@ public class InputManagerService extends IInputManager.Stub } @Override - public boolean setVirtualMousePointerDisplayId(int pointerDisplayId) { - return InputManagerService.this - .setVirtualMousePointerDisplayIdBlocking(pointerDisplayId); - } - - @Override - public int getVirtualMousePointerDisplayId() { - return InputManagerService.this.getVirtualMousePointerDisplayId(); - } - - @Override public PointF getCursorPosition(int displayId) { final float[] p = mNative.getMouseCursorPosition(displayId); if (p == null || p.length != 2) { @@ -3413,44 +3273,6 @@ public class InputManagerService extends IInputManager.Stub } } - private void applyAdditionalDisplayInputProperties() { - synchronized (mAdditionalDisplayInputPropertiesLock) { - AdditionalDisplayInputProperties properties = - mAdditionalDisplayInputProperties.get(mRequestedPointerDisplayId); - if (properties == null) properties = DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES; - applyAdditionalDisplayInputPropertiesLocked(properties); - } - } - - @GuardedBy("mAdditionalDisplayInputPropertiesLock") - private void applyAdditionalDisplayInputPropertiesLocked( - AdditionalDisplayInputProperties properties) { - // Handle changes to each of the individual properties. - // TODO(b/293587049): This approach for updating pointer display properties is only for when - // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is - // permanently enabled. - - if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) { - mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible; - if (properties.pointerIconVisible) { - if (mPointerIconType == PointerIcon.TYPE_CUSTOM) { - Objects.requireNonNull(mPointerIcon); - mNative.setCustomPointerIcon(mPointerIcon); - } else { - mNative.setPointerIconType(mPointerIconType); - } - } else { - mNative.setPointerIconType(PointerIcon.TYPE_NULL); - } - } - - if (properties.mousePointerAccelerationEnabled - != mCurrentDisplayProperties.mousePointerAccelerationEnabled) { - mCurrentDisplayProperties.mousePointerAccelerationEnabled = - properties.mousePointerAccelerationEnabled; - } - } - private void updateAdditionalDisplayInputProperties(int displayId, Consumer<AdditionalDisplayInputProperties> updater) { synchronized (mAdditionalDisplayInputPropertiesLock) { @@ -3473,13 +3295,6 @@ public class InputManagerService extends IInputManager.Stub if (properties.allDefaults()) { mAdditionalDisplayInputProperties.remove(displayId); } - if (displayId != mRequestedPointerDisplayId) { - Log.i(TAG, "Not applying additional properties for display " + displayId - + " because the pointer is currently targeting display " - + mRequestedPointerDisplayId + "."); - return; - } - applyAdditionalDisplayInputPropertiesLocked(properties); } } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 32d5044d9e41..f742360484f5 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -189,12 +189,8 @@ interface NativeInputManagerService { void disableInputDevice(int deviceId); - void setPointerIconType(int iconId); - void reloadPointerIcons(); - void setCustomPointerIcon(@NonNull PointerIcon icon); - boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId, @NonNull IBinder inputToken); @@ -467,15 +463,9 @@ interface NativeInputManagerService { public native void disableInputDevice(int deviceId); @Override - public native void setPointerIconType(int iconId); - - @Override public native void reloadPointerIcons(); @Override - public native void setCustomPointerIcon(PointerIcon icon); - - @Override public native boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId, IBinder inputToken); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 1b9d6c598545..0fde760fd02a 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -2234,7 +2234,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. mInputManagerInternal.notifyInputMethodConnectionActive(connectionIsActive); } final var userData = mUserDataRepository.getOrCreate(mCurrentUserId); - + final var bindingController = userData.mBindingController; // If configured, we want to avoid starting up the IME if it is not supposed to be showing if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags, @@ -2243,7 +2243,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. Slog.d(TAG, "Avoiding IME startup and unbinding current input method."); } invalidateAutofillSessionLocked(); - userData.mBindingController.unbindCurrentMethod(); + bindingController.unbindCurrentMethod(); return InputBindResult.NO_EDITOR; } @@ -2275,8 +2275,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. } } - userData.mBindingController.unbindCurrentMethod(); - return userData.mBindingController.bindCurrentMethod(); + bindingController.unbindCurrentMethod(); + return bindingController.bindCurrentMethod(); } /** diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 573116105dea..d6d134d892f5 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -78,10 +78,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** @@ -152,6 +156,16 @@ public class ContextHubService extends IContextHubService.Stub { private final ScheduledThreadPoolExecutor mDailyMetricTimer = new ScheduledThreadPoolExecutor(1); + // A queue of reliable message records for duplicate detection + private final PriorityQueue<ReliableMessageRecord> mReliableMessageRecordQueue = + new PriorityQueue<ReliableMessageRecord>( + (ReliableMessageRecord left, ReliableMessageRecord right) -> { + return Long.compare(left.getTimestamp(), right.getTimestamp()); + }); + + // The test mode manager that manages behaviors during test mode. + private final TestModeManager mTestModeManager = new TestModeManager(); + // The period of the recurring time private static final int PERIOD_METRIC_QUERY_DAYS = 1; @@ -164,6 +178,9 @@ public class ContextHubService extends IContextHubService.Stub { private boolean mIsBtScanningEnabled = false; private boolean mIsBtMainEnabled = false; + // True if test mode is enabled for the Context Hub + private AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false); + // A hashmap used to record if a contexthub is waiting for daily query private Set<Integer> mMetricQueryPendingContextHubIds = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>()); @@ -210,8 +227,17 @@ public class ContextHubService extends IContextHubService.Stub { @Override public void handleNanoappMessage(short hostEndpointId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions) { - handleClientMessageCallback(mContextHubId, hostEndpointId, message, nanoappPermissions, - messagePermissions); + if (Flags.reliableMessageImplementation() + && Flags.reliableMessageTestModeBehavior() + && mIsTestModeEnabled.get() + && mTestModeManager.handleNanoappMessage(mContextHubId, hostEndpointId, + message, nanoappPermissions, messagePermissions)) { + // The TestModeManager handled the nanoapp message, so return here. + return; + } + + handleClientMessageCallback(mContextHubId, hostEndpointId, message, + nanoappPermissions, messagePermissions); } @Override @@ -228,6 +254,106 @@ public class ContextHubService extends IContextHubService.Stub { } } + /** + * Records a reliable message from a nanoapp for duplicate detection. + */ + private static class ReliableMessageRecord { + public static final int TIMEOUT_NS = 1000000000; + + public int mContextHubId; + public long mTimestamp; + public int mMessageSequenceNumber; + byte mErrorCode; + + ReliableMessageRecord(int contextHubId, long timestamp, + int messageSequenceNumber, byte errorCode) { + mContextHubId = contextHubId; + mTimestamp = timestamp; + mMessageSequenceNumber = messageSequenceNumber; + mErrorCode = errorCode; + } + + public int getContextHubId() { + return mContextHubId; + } + + public long getTimestamp() { + return mTimestamp; + } + + public int getMessageSequenceNumber() { + return mMessageSequenceNumber; + } + + public byte getErrorCode() { + return mErrorCode; + } + + public void setErrorCode(byte errorCode) { + mErrorCode = errorCode; + } + + public boolean isExpired() { + return mTimestamp + TIMEOUT_NS < SystemClock.elapsedRealtimeNanos(); + } + } + + /** + * A class to manage behaviors during test mode. This is used for testing. + */ + private class TestModeManager { + /** + * Probability (in percent) of duplicating a message. + */ + private static final int MESSAGE_DUPLICATION_PROBABILITY_PERCENT = 50; + + /** + * The number of total messages to send when the duplicate event happens. + */ + private static final int NUM_MESSAGES_TO_DUPLICATE = 3; + + /** + * A probability percent for a certain event. + */ + private static final int MAX_PROBABILITY_PERCENT = 100; + + /** + * Random number generator. + */ + private Random mRandom = new Random(); + + /** + * @see ContextHubServiceCallback.handleNanoappMessage + * @return whether the message was handled + */ + public boolean handleNanoappMessage(int contextHubId, + short hostEndpointId, NanoAppMessage message, + List<String> nanoappPermissions, List<String> messagePermissions) { + if (!message.isReliable()) { + return false; + } + + if (didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) { + for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) { + handleClientMessageCallback(contextHubId, hostEndpointId, + message, nanoappPermissions, messagePermissions); + } + return true; + } + return false; + } + + /** + * Returns true if the event with percentPercent did happen. + * + * @param probabilityPercent the percent probability of the event. + * @return true if the event happened, false otherwise. + */ + private boolean didEventHappen(int probabilityPercent) { + return mRandom.nextInt(MAX_PROBABILITY_PERCENT) < probabilityPercent; + } + } + public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) { Log.i(TAG, "Starting Context Hub Service init"); mContext = context; @@ -563,6 +689,8 @@ public class ContextHubService extends IContextHubService.Stub { * Resets the settings. Called when a context hub restarts or the AIDL HAL dies */ private void resetSettings() { + mIsTestModeEnabled.set(false); + sendLocationSettingUpdate(); sendWifiSettingUpdate(/* forceUpdate= */ true); sendAirplaneModeSettingUpdate(); @@ -854,14 +982,76 @@ public class ContextHubService extends IContextHubService.Stub { private void handleClientMessageCallback(int contextHubId, short hostEndpointId, NanoAppMessage message, List<String> nanoappPermissions, List<String> messagePermissions) { - byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId, message, - nanoappPermissions, messagePermissions); - if (message.isReliable() && errorCode != ErrorCode.OK) { - sendMessageDeliveryStatusToContextHub(contextHubId, message.getMessageSequenceNumber(), - errorCode); + if (!Flags.reliableMessageImplementation() + || !Flags.reliableMessageDuplicateDetectionService()) { + byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId, + message, nanoappPermissions, messagePermissions); + if (message.isReliable() && errorCode != ErrorCode.OK) { + sendMessageDeliveryStatusToContextHub(contextHubId, + message.getMessageSequenceNumber(), errorCode); + } + return; + } + + if (message.isReliable()) { + byte errorCode = ErrorCode.OK; + synchronized (mReliableMessageRecordQueue) { + Optional<ReliableMessageRecord> record = Optional.empty(); + for (ReliableMessageRecord r: mReliableMessageRecordQueue) { + if (r.getContextHubId() == contextHubId + && r.getMessageSequenceNumber() == message.getMessageSequenceNumber()) { + record = Optional.of(r); + break; + } + } + + if (record.isPresent()) { + errorCode = record.get().getErrorCode(); + if (errorCode == ErrorCode.TRANSIENT_ERROR) { + Log.w(TAG, "Found duplicate reliable message with message sequence number: " + + record.get().getMessageSequenceNumber() + ": retrying"); + errorCode = mClientManager.onMessageFromNanoApp( + contextHubId, hostEndpointId, message, + nanoappPermissions, messagePermissions); + record.get().setErrorCode(errorCode); + } else { + Log.w(TAG, "Found duplicate reliable message with message sequence number: " + + record.get().getMessageSequenceNumber()); + } + } else { + errorCode = mClientManager.onMessageFromNanoApp( + contextHubId, hostEndpointId, message, + nanoappPermissions, messagePermissions); + mReliableMessageRecordQueue.add( + new ReliableMessageRecord(contextHubId, + SystemClock.elapsedRealtimeNanos(), + message.getMessageSequenceNumber(), + errorCode)); + } + } + sendMessageDeliveryStatusToContextHub(contextHubId, + message.getMessageSequenceNumber(), errorCode); + } else { + mClientManager.onMessageFromNanoApp( + contextHubId, hostEndpointId, message, + nanoappPermissions, messagePermissions); + } + + synchronized (mReliableMessageRecordQueue) { + while (mReliableMessageRecordQueue.peek() != null + && mReliableMessageRecordQueue.peek().isExpired()) { + mReliableMessageRecordQueue.poll(); + } } } + /** + * Sends the message delivery status to the Context Hub. + * + * @param contextHubId the ID of the hub + * @param messageSequenceNumber the message sequence number + * @param errorCode the error code, one of the enum ErrorCode + */ private void sendMessageDeliveryStatusToContextHub(int contextHubId, int messageSequenceNumber, byte errorCode) { if (!Flags.reliableMessageImplementation()) { @@ -1229,6 +1419,9 @@ public class ContextHubService extends IContextHubService.Stub { public boolean setTestMode(boolean enable) { super.setTestMode_enforcePermission(); boolean status = mContextHubWrapper.setTestMode(enable); + if (status) { + mIsTestModeEnabled.set(enable); + } // Query nanoapps to update service state after test mode state change. for (int contextHubId: mDefaultClientMap.keySet()) { diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 869b89a6670c..73647dbbe978 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -1305,22 +1305,20 @@ class MediaRouter2ServiceImpl { route.getId(), requestId)); + UserHandler userHandler = routerRecord.mUserRecord.mHandler; if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) { - ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId( - toRequesterId(managerRequestId)); + ManagerRecord manager = userHandler.findManagerWithId(toRequesterId(managerRequestId)); if (manager == null || manager.mLastSessionCreationRequest == null) { Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + "Ignoring unknown request."); - routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( - routerRecord, requestId); + userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId); return; } if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(), oldSession.getId())) { Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + "Ignoring unmatched routing session."); - routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( - routerRecord, requestId); + userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId); return; } if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(), @@ -1333,29 +1331,28 @@ class MediaRouter2ServiceImpl { } else { Slog.w(TAG, "requestCreateSessionWithRouter2Locked: " + "Ignoring unmatched route."); - routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( - routerRecord, requestId); + userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId); return; } } manager.mLastSessionCreationRequest = null; } else { + String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId(); if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission() - && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) { + && !TextUtils.equals(route.getId(), defaultRouteId)) { Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to" + route); - routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter( - routerRecord, requestId); + userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId); return; } } long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); - routerRecord.mUserRecord.mHandler.sendMessage( + userHandler.sendMessage( obtainMessage( UserHandler::requestCreateSessionWithRouter2OnHandler, - routerRecord.mUserRecord.mHandler, + userHandler, uniqueRequestId, managerRequestId, transferInitiatorUserHandle, @@ -1429,18 +1426,22 @@ class MediaRouter2ServiceImpl { "transferToRouteWithRouter2 | router: %s(id: %d), route: %s", routerRecord.mPackageName, routerRecord.mRouterId, route.getId())); + UserHandler userHandler = routerRecord.mUserRecord.mHandler; + String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId(); if (route.isSystemRoute() && !routerRecord.hasSystemRoutingPermission() - && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) { - routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::notifySessionCreationFailedToRouter, - routerRecord.mUserRecord.mHandler, - routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID))); + && !TextUtils.equals(route.getId(), defaultRouteId)) { + userHandler.sendMessage( + obtainMessage( + UserHandler::notifySessionCreationFailedToRouter, + userHandler, + routerRecord, + toOriginalRequestId(DUMMY_REQUEST_ID))); } else { - routerRecord.mUserRecord.mHandler.sendMessage( + userHandler.sendMessage( obtainMessage( UserHandler::transferToRouteOnHandler, - routerRecord.mUserRecord.mHandler, + userHandler, DUMMY_REQUEST_ID, transferInitiatorUserHandle, routerRecord.mPackageName, diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index c105b9c37026..6ce3ab4b2d65 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -232,10 +232,16 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { String sessionId, String routeId, @RoutingSessionInfo.TransferReason int transferReason) { + String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId(); if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) { - // The currently selected route is the default route. - Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT); - return; + if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { + // Transfer to the default route (which is the selected route). We replace the id to + // be the selected route id so that the transfer reason gets updated. + routeId = selectedDeviceRouteId; + } else { + Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT); + return; + } } if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { @@ -250,11 +256,11 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } } - MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute(); + String finalRouteId = routeId; // Make a final copy to use it in the lambda. boolean isAvailableDeviceRoute = mDeviceRouteController.getAvailableRoutes().stream() - .anyMatch(it -> it.getId().equals(routeId)); - boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId()); + .anyMatch(it -> it.getId().equals(finalRouteId)); + boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId); if (isSelectedDeviceRoute || isAvailableDeviceRoute) { // The requested route is managed by the device route controller. Note that the selected diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 143bc5cb20ff..b589f4972155 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -486,6 +486,7 @@ public class ZenModeHelper { } } + @GuardedBy("mConfigLock") private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd, AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) { if (!Flags.modesApi()) { @@ -1112,6 +1113,7 @@ public class ZenModeHelper { * <p>The rule's {@link ZenRule#condition} is cleared (meaning that an active rule will be * deactivated) unless the update has origin == {@link ZenModeConfig#UPDATE_ORIGIN_USER}. */ + @GuardedBy("mConfigLock") private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule, @ConfigChangeOrigin int origin, boolean isNew) { if (Flags.modesApi()) { @@ -1261,12 +1263,14 @@ public class ZenModeHelper { * * <p>Returns {@code true} if the policy of the rule was modified. */ + @GuardedBy("mConfigLock") private boolean updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, boolean updateBitmask, boolean isNew) { if (newPolicy == null) { if (isNew) { // Newly created rule with no provided policy; fill in with the default. - zenRule.zenPolicy = mDefaultConfig.toZenPolicy(); + zenRule.zenPolicy = + Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy(); return true; } // Otherwise, a null policy means no policy changes, so we can stop here. @@ -1275,8 +1279,9 @@ public class ZenModeHelper { // If oldPolicy is null, we compare against the default policy when determining which // fields in the bitmask should be marked as updated. - ZenPolicy oldPolicy = - zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy(); + ZenPolicy oldPolicy = zenRule.zenPolicy != null + ? zenRule.zenPolicy + : (Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy()); // If this is updating a rule rather than creating a new one, keep any fields from the // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy @@ -2033,7 +2038,8 @@ public class ZenModeHelper { // rule's policy fields should be set upon creation, this is a fallback to // catch any that may have fallen through the cracks. Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); - policy.apply(mDefaultConfig.toZenPolicy()); + policy.apply( + Flags.modesUi() ? mDefaultConfig.toZenPolicy() : mConfig.toZenPolicy()); } } else { // active rule with no specified policy inherits the global config settings diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index c60f0afcc2ff..209cbb7f591e 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -46,6 +46,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApexStagedEvent; +import android.content.pm.Flags; import android.content.pm.IPackageManagerNative; import android.content.pm.IStagedApexObserver; import android.content.pm.PackageManager; @@ -766,6 +767,10 @@ public final class DexOptHelper { final PackageSetting ps = installRequest.getScannedPackageSetting(); final AndroidPackage pkg = ps.getPkg(); final boolean onIncremental = isIncrementalPath(ps.getPathString()); + final boolean performDexOptForRollback = Flags.recoverabilityDetection() + ? !(installRequest.isRollback() + && installRequest.getInstallSource().mInitiatingPackageName.equals("android")) + : true; return (!instantApp || Global.getInt(context.getContentResolver(), Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0) @@ -773,7 +778,8 @@ public final class DexOptHelper { && !pkg.isDebuggable() && (!onIncremental) && dexoptOptions.isCompilationEnabled() - && !isApex; + && !isApex + && performDexOptForRollback; } private static class StagedApexObserver extends IStagedApexObserver.Stub { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 179379487aba..7a36f6dabe06 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -35,6 +35,8 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.role.RoleManager; +import android.app.usage.StorageStats; +import android.app.usage.StorageStatsManager; import android.content.ComponentName; import android.content.Context; import android.content.IIntentReceiver; @@ -136,6 +138,7 @@ import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.URISyntaxException; import java.security.SecureRandom; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -275,6 +278,8 @@ class PackageManagerShellCommand extends ShellCommand { return runClear(); case "get-archived-package-metadata": return runGetArchivedPackageMetadata(); + case "get-package-storage-stats": + return runGetPackageStorageStats(); case "install-archived": return runArchivedInstall(); case "enable": @@ -1861,6 +1866,103 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + /** + * Returns a string that shows the number of bytes in b, Kb, Mb or Gb. + */ + protected static String getFormattedBytes(long size) { + double k = size/1024.0; + double m = size/1048576.0; + double g = size/1073741824.0; + + DecimalFormat dec = new DecimalFormat("0.00"); + if (g > 1) { + return dec.format(g).concat(" Gb"); + } else if (m > 1) { + return dec.format(m).concat(" Mb"); + } else if (k > 1) { + return dec.format(k).concat(" Kb"); + } + return ""; + } + + /** + * Return the string that displays the data size. + */ + private String getDataSizeDisplay(long size) { + String formattedOutput = getFormattedBytes(size); + if (!formattedOutput.isEmpty()) { + formattedOutput = " (" + formattedOutput + ")"; + } + return Long.toString(size) + " bytes" + formattedOutput; + } + + /** + * Display storage stats of the specified package. + * + * Usage: get-package-storage-stats [--usr USER_ID] PACKAGE + */ + private int runGetPackageStorageStats() throws RemoteException { + final PrintWriter pw = getOutPrintWriter(); + if (!android.content.pm.Flags.getPackageStorageStats()) { + pw.println("Error: get_package_storage_stats flag is not enabled"); + return 1; + } + if (!android.app.usage.Flags.getAppBytesByDataTypeApi()) { + pw.println("Error: get_app_bytes_by_data_type_api flag is not enabled"); + return 1; + } + int userId = UserHandle.USER_CURRENT; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + default: + pw.println("Error: Unknown option: " + opt); + return 1; + } + } + + final String packageName = getNextArg(); + if (packageName == null) { + pw.println("Error: package name not specified"); + return 1; + } + try { + StorageStatsManager storageStatsManager = + mContext.getSystemService(StorageStatsManager.class); + final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL, + "runGetPackageStorageStats"); + StorageStats stats = + storageStatsManager.queryStatsForPackage(StorageManager.UUID_DEFAULT, + packageName, UserHandle.of(translatedUserId)); + + pw.println("code: " + getDataSizeDisplay(stats.getAppBytes())); + pw.println("data: " + getDataSizeDisplay(stats.getDataBytes())); + pw.println("cache: " + getDataSizeDisplay(stats.getCacheBytes())); + pw.println("apk: " + getDataSizeDisplay(stats.getAppBytesByDataType( + StorageStats.APP_DATA_TYPE_FILE_TYPE_APK))); + pw.println("lib: " + getDataSizeDisplay( + stats.getAppBytesByDataType(StorageStats.APP_DATA_TYPE_LIB))); + pw.println("dm: " + getDataSizeDisplay(stats.getAppBytesByDataType( + StorageStats.APP_DATA_TYPE_FILE_TYPE_DM))); + pw.println("dexopt artifacts: " + getDataSizeDisplay(stats.getAppBytesByDataType( + StorageStats.APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT))); + pw.println("current profile : " + getDataSizeDisplay(stats.getAppBytesByDataType( + StorageStats.APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE))); + pw.println("reference profile: " + getDataSizeDisplay(stats.getAppBytesByDataType( + StorageStats.APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE))); + pw.println("external cache: " + getDataSizeDisplay(stats.getExternalCacheBytes())); + } catch (Exception e) { + getErrPrintWriter().println("Failed to get storage stats, reason: " + e); + pw.println("Failure [failed to get storage stats], reason: " + e); + return -1; + } + return 0; + } + private int runInstallExisting() throws RemoteException { final PrintWriter pw = getOutPrintWriter(); int userId = UserHandle.USER_CURRENT; @@ -4869,6 +4971,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" Displays the component name of the domain verification agent on device."); pw.println(" If the component isn't enabled, an error message will be displayed."); pw.println(" --user: return the agent of the given user (SYSTEM_USER if unspecified)"); + pw.println(" get-package-storage-stats [--user <USER_ID>] <PACKAGE>"); + pw.println(" Return the storage stats for the given app, if present"); pw.println(""); printArtServiceHelp(); pw.println(""); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index b1976cd0d13b..f6487ceea43b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1371,7 +1371,7 @@ public class UserManagerService extends IUserManager.Stub { for (int i = 0; i < userSize; i++) { UserInfo ui = mUsers.valueAt(i).info; if ((excludePartial && ui.partial) - || (excludeDying && mRemovingUserIds.get(ui.id)) + || (excludeDying && isDyingLU(ui)) || (excludePreCreated && ui.preCreated)) { continue; } @@ -1381,6 +1381,17 @@ public class UserManagerService extends IUserManager.Stub { } } + @GuardedBy("mUsersLock") + private boolean isDyingLU(UserInfo ui) { + if (mRemovingUserIds.get(ui.id)) { + return true; + } + if (ui.isEphemeral() && ui.isInitialized() && ui.id != getCurrentUserId()) { + return true; + } + return false; + } + @Override public List<UserInfo> getProfiles(@UserIdInt int userId, boolean enabledOnly) { boolean returnFullInfo; diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index 1a9e012a7c53..f7eb29fe3ee9 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -141,6 +141,8 @@ final class VerifyingSession { @NonNull private final PackageManagerService mPm; + private final int mInstallReason; + VerifyingSession(UserHandle user, File stagedDir, IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams, InstallSource installSource, int installerUid, SigningDetails signingDetails, int sessionId, PackageLite lite, @@ -168,6 +170,7 @@ final class VerifyingSession { mUserActionRequiredType = sessionParams.requireUserAction; mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING; mIsStaged = sessionParams.isStaged; + mInstallReason = sessionParams.installReason; } @Override @@ -190,7 +193,9 @@ final class VerifyingSession { // Perform package verification and enable rollback (unless we are simply moving the // package). if (!mOriginInfo.mExisting) { - if (!isApex() && !isArchivedInstallation()) { + final boolean verifyForRollback = Flags.recoverabilityDetection() + ? !isARollback() : true; + if (!isApex() && !isArchivedInstallation() && verifyForRollback) { // TODO(b/182426975): treat APEX as APK when APK verification is concerned sendApkVerificationRequest(pkgLite); } @@ -200,6 +205,11 @@ final class VerifyingSession { } } + private boolean isARollback() { + return mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK + && mInstallSource.mInitiatingPackageName.equals("android"); + } + private void sendApkVerificationRequest(PackageInfoLite pkgLite) { final int verificationId = mPm.mPendingVerificationToken++; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 5e95a4b79d00..202e94c6ec84 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -7401,6 +7401,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } static boolean isPip2ExperimentEnabled() { - return Flags.enablePip2Implementation(); + return Flags.enablePip2Implementation() || SystemProperties.getBoolean( + "wm_shell.pip2", false); } } diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 0e446b8eaf8c..eb1f3b402364 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -39,6 +39,7 @@ import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONL import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg; +import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balRequireOptInSameUid; @@ -805,14 +806,25 @@ public class BackgroundActivityStartController { * or {@link #BAL_BLOCK} if the launch should be blocked */ BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) { - int callingUid = state.mCallingUid; - int callingPid = state.mCallingPid; - final String callingPackage = state.mCallingPackage; - WindowProcessController callerApp = state.mCallerApp; + // This is used to block background activity launch even if the app is still + // visible to user after user clicking home button. + + // Normal apps with visible app window will be allowed to start activity if app switching + // is allowed, or apps like live wallpaper with non app visible window will be allowed. + final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW + || state.mAppSwitchState == APP_SWITCH_FG_ONLY; + if (appSwitchAllowedOrFg && state.mCallingUidHasAnyVisibleWindow) { + return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, + /*background*/ false, "callingUid has visible window"); + } + if (mService.mActiveUids.hasNonAppVisibleWindow(state.mCallingUid)) { + return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, + /*background*/ false, "callingUid has non-app visible window"); + } // don't abort for the most important UIDs - final int callingAppId = UserHandle.getAppId(callingUid); - if (callingUid == Process.ROOT_UID + final int callingAppId = UserHandle.getAppId(state.mCallingUid); + if (state.mCallingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID || callingAppId == Process.NFC_UID) { return new BalVerdict( @@ -821,7 +833,7 @@ public class BackgroundActivityStartController { } // Always allow home application to start activities. - if (isHomeApp(callingUid, callingPackage)) { + if (isHomeApp(state.mCallingUid, state.mCallingPackage)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ false, "Home app"); @@ -836,67 +848,46 @@ public class BackgroundActivityStartController { "Active ime"); } - // This is used to block background activity launch even if the app is still - // visible to user after user clicking home button. - final int appSwitchState = mService.getBalAppSwitchesState(); - - // don't abort if the callingUid has a visible window or is a persistent system process - final int callingUidProcState = mService.mActiveUids.getUidState(callingUid); - final boolean callingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid); - final boolean isCallingUidPersistentSystemProcess = - callingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI; - - // Normal apps with visible app window will be allowed to start activity if app switching - // is allowed, or apps like live wallpaper with non app visible window will be allowed. - final boolean appSwitchAllowedOrFg = - appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY; - if (appSwitchAllowedOrFg && callingUidHasAnyVisibleWindow) { - return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, - /*background*/ false, "callingUid has visible window"); - } - if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) { - return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW, - /*background*/ false, "callingUid has non-app visible window"); - } - - if (isCallingUidPersistentSystemProcess) { + // don't abort if the callingUid is a persistent system process + if (state.mIsCallingUidPersistentSystemProcess) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ false, "callingUid is persistent system process"); } // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission - if (hasBalPermission(callingUid, callingPid)) { + if (hasBalPermission(state.mCallingUid, state.mCallingPid)) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, "START_ACTIVITIES_FROM_BACKGROUND permission granted"); } // don't abort if the caller has the same uid as the recents component - if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) { + if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Recents Component"); } // don't abort if the callingUid is the device owner - if (mService.isDeviceOwner(callingUid)) { + if (mService.isDeviceOwner(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Device Owner"); } // don't abort if the callingUid is a affiliated profile owner - if (mService.isAffiliatedProfileOwner(callingUid)) { + if (mService.isAffiliatedProfileOwner(state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Affiliated Profile Owner"); } // don't abort if the callingUid has companion device - final int callingUserId = UserHandle.getUserId(callingUid); - if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) { + final int callingUserId = UserHandle.getUserId(state.mCallingUid); + if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT, /*background*/ true, "Companion App"); } // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission - if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { + if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid, + state.mCallingPackage)) { Slog.w( TAG, "Background activity start for " - + callingPackage + + state.mCallingPackage + " allowed because SYSTEM_ALERT_WINDOW permission is granted."); return new BalVerdict(BAL_ALLOW_SAW_PERMISSION, /*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted"); @@ -905,7 +896,7 @@ public class BackgroundActivityStartController { // OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow( AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION, - callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) { + state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true, "OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted"); } @@ -914,7 +905,7 @@ public class BackgroundActivityStartController { // That's the case for PendingIntent-based starts, since the creator's process might not be // up and alive. // Don't abort if the callerApp or other processes of that uid are allowed in any way. - BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state); + BalVerdict callerAppAllowsBal = checkProcessAllowsBal(state.mCallerApp, state); if (callerAppAllowsBal.allows()) { return callerAppAllowsBal; } @@ -929,13 +920,6 @@ public class BackgroundActivityStartController { */ BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) { - if (state.isPendingIntentBalAllowedByPermission() - && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) { - return new BalVerdict(BAL_ALLOW_PERMISSION, - /*background*/ false, - "realCallingUid has BAL permission."); - } - // Normal apps with visible app window will be allowed to start activity if app switching // is allowed, or apps like live wallpaper with non app visible window will be allowed. // The home app can start apps even if app switches are usually disallowed. @@ -961,6 +945,13 @@ public class BackgroundActivityStartController { } } + if (state.isPendingIntentBalAllowedByPermission() + && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) { + return new BalVerdict(BAL_ALLOW_PERMISSION, + /*background*/ false, + "realCallingUid has BAL permission."); + } + // if the realCallingUid is a persistent system process, abort if the IntentSender // wasn't allowed to start an activity if (state.mForcedBalByPiSender.allowsBackgroundActivityStarts() @@ -1660,26 +1651,61 @@ public class BackgroundActivityStartController { (state.mOriginatingPendingIntent != null)); } - @BalCode int code = finalVerdict.getCode(); - int callingUid = state.mCallingUid; - int realCallingUid = state.mRealCallingUid; - Intent intent = state.mIntent; - - if (code == BAL_ALLOW_PENDING_INTENT - && (callingUid < Process.FIRST_APPLICATION_UID - || realCallingUid < Process.FIRST_APPLICATION_UID)) { - String activityName = intent != null - ? requireNonNull(intent.getComponent()).flattenToShortString() : ""; - writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT, - state); + if (balImprovedMetrics()) { + if (shouldLogStats(finalVerdict, state)) { + String activityName; + if (shouldLogIntentActivity(finalVerdict, state)) { + Intent intent = state.mIntent; + activityName = intent == null ? "noIntent" // should never happen + : requireNonNull(intent.getComponent()).flattenToShortString(); + } else { + activityName = ""; + } + writeBalAllowedLog(activityName, finalVerdict.getCode(), state); + } + } else { + @BalCode int code = finalVerdict.getCode(); + int callingUid = state.mCallingUid; + int realCallingUid = state.mRealCallingUid; + Intent intent = state.mIntent; + + if (code == BAL_ALLOW_PENDING_INTENT + && (callingUid < Process.FIRST_APPLICATION_UID + || realCallingUid < Process.FIRST_APPLICATION_UID)) { + String activityName = intent != null + ? requireNonNull(intent.getComponent()).flattenToShortString() : ""; + writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT, + state); + } + if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND + || code == BAL_ALLOW_SAW_PERMISSION) { + // We don't need to know which activity in this case. + writeBalAllowedLog("", code, state); + } } - if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND - || code == BAL_ALLOW_SAW_PERMISSION) { - // We don't need to know which activity in this case. - writeBalAllowedLog("", code, state); + return finalVerdict; + } + @VisibleForTesting + boolean shouldLogStats(BalVerdict finalVerdict, BalState state) { + if (finalVerdict.blocks()) { + return false; } - return finalVerdict; + if (!state.isPendingIntent() && finalVerdict.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) { + return false; + } + if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts() + && state.mResultForRealCaller.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) { + return false; + } + return true; + } + + @VisibleForTesting + boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) { + return finalVerdict.mBasedOnRealCaller + ? state.mRealCallingUid < Process.FIRST_APPLICATION_UID + : state.mCallingUid < Process.FIRST_APPLICATION_UID; } @VisibleForTesting void writeBalAllowedLog(String activityName, int code, BalState state) { diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 5aa0ed7ce76c..ce1a72deb523 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -25,7 +25,6 @@ import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; import android.provider.DeviceConfig; -import android.util.Slog; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -33,6 +32,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.function.Function; +import java.util.function.IntSupplier; /** Reads letterbox configs from resources and controls their overrides at runtime. */ final class LetterboxConfiguration { @@ -265,6 +265,12 @@ final class LetterboxConfiguration { // unresizable apps private boolean mIsDisplayAspectRatioEnabledForFixedOrientationLetterbox; + // Supplier for the value in pixel to consider when detecting vertical thin letterboxing + private final IntSupplier mThinLetterboxWidthFn; + + // Supplier for the value in pixel to consider when detecting horizontal thin letterboxing + private final IntSupplier mThinLetterboxHeightFn; + // Allows to enable letterboxing strategy for translucent activities ignoring flags. private boolean mTranslucentLetterboxingOverrideEnabled; @@ -358,6 +364,10 @@ final class LetterboxConfiguration { R.bool.config_isWindowManagerCameraCompatSplitScreenAspectRatioEnabled); mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled); + mThinLetterboxWidthFn = () -> mContext.getResources().getDimensionPixelSize( + R.dimen.config_letterboxThinLetterboxWidthDp); + mThinLetterboxHeightFn = () -> mContext.getResources().getDimensionPixelSize( + R.dimen.config_letterboxThinLetterboxHeightDp); mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); @@ -1129,6 +1139,24 @@ final class LetterboxConfiguration { } /** + * @return Width in pixel about the padding to use to understand if the letterbox for an + * activity is thin. If the available space has width W and the app has width w, this + * is the maximum value for (W - w) / 2 to be considered for a thin letterboxed app. + */ + int getThinLetterboxWidthPx() { + return mThinLetterboxWidthFn.getAsInt(); + } + + /** + * @return Height in pixel about the padding to use to understand if a letterbox is thin. + * If the available space has height H and the app has height h, this is the maximum + * value for (H - h) / 2 to be considered for a thin letterboxed app. + */ + int getThinLetterboxHeightPx() { + return mThinLetterboxHeightFn.getAsInt(); + } + + /** * Overrides whether using split screen aspect ratio as a default aspect ratio for unresizable * apps. */ diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index b38e666ea691..9e16b8abe0de 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -1024,6 +1024,67 @@ final class LetterboxUiController { return getSplitScreenAspectRatio(); } + /** + * @return {@value true} if the resulting app is letterboxed in a way defined as thin. + */ + boolean isVerticalThinLetterboxed() { + final int thinHeight = mLetterboxConfiguration.getThinLetterboxHeightPx(); + if (thinHeight < 0) { + return false; + } + final Task task = mActivityRecord.getTask(); + if (task == null) { + return false; + } + final int padding = Math.abs( + task.getBounds().height() - mActivityRecord.getBounds().height()) / 2; + return padding <= thinHeight; + } + + /** + * @return {@value true} if the resulting app is pillarboxed in a way defined as thin. + */ + boolean isHorizontalThinLetterboxed() { + final int thinWidth = mLetterboxConfiguration.getThinLetterboxWidthPx(); + if (thinWidth < 0) { + return false; + } + final Task task = mActivityRecord.getTask(); + if (task == null) { + return false; + } + final int padding = Math.abs( + task.getBounds().width() - mActivityRecord.getBounds().width()) / 2; + return padding <= thinWidth; + } + + + /** + * @return {@value true} if the vertical reachability should be allowed in case of + * thin letteboxing + */ + boolean allowVerticalReachabilityForThinLetterbox() { + if (!Flags.disableThinLetterboxingReachability()) { + return true; + } + // When the flag is enabled we allow vertical reachability only if the + // app is not thin letterboxed vertically. + return !isVerticalThinLetterboxed(); + } + + /** + * @return {@value true} if the vertical reachability should be enabled in case of + * thin letteboxing + */ + boolean allowHorizontalReachabilityForThinLetterbox() { + if (!Flags.disableThinLetterboxingReachability()) { + return true; + } + // When the flag is enabled we allow horizontal reachability only if the + // app is not thin pillarboxed. + return !isHorizontalThinLetterboxed(); + } + float getSplitScreenAspectRatio() { // Getting the same aspect ratio that apps get in split screen. final DisplayArea displayArea = mActivityRecord.getDisplayArea(); @@ -1263,6 +1324,9 @@ final class LetterboxUiController { * </ul> */ private boolean isHorizontalReachabilityEnabled(Configuration parentConfiguration) { + if (!allowHorizontalReachabilityForThinLetterbox()) { + return false; + } // Use screen resolved bounds which uses resolved bounds or size compat bounds // as activity bounds can sometimes be empty final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior() @@ -1298,6 +1362,9 @@ final class LetterboxUiController { * </ul> */ private boolean isVerticalReachabilityEnabled(Configuration parentConfiguration) { + if (!allowVerticalReachabilityForThinLetterbox()) { + return false; + } // Use screen resolved bounds which uses resolved bounds or size compat bounds // as activity bounds can sometimes be empty final Rect opaqueActivityBounds = hasInheritedLetterboxBehavior() @@ -1566,6 +1633,8 @@ final class LetterboxUiController { if (!shouldShowLetterboxUi) { return; } + pw.println(prefix + " isVerticalThinLetterboxed=" + isVerticalThinLetterboxed()); + pw.println(prefix + " isHorizontalThinLetterboxed=" + isHorizontalThinLetterboxed()); pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString( getLetterboxBackgroundColor().toArgb())); pw.println(prefix + " letterboxBackgroundType=" diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index ce47f5cc8a7e..60454fc5d7c6 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -18,6 +18,7 @@ rgl@google.com yunfanc@google.com wilsonshih@google.com jiamingliu@google.com +pdwilliams@google.com # Files related to background activity launches per-file Background*Start* = set noparent diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a9c47b89b8ff..8bd7b5f78cf4 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -20,7 +20,6 @@ import static android.app.ActivityManager.isStartResultSuccessful; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE; import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED; -import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; @@ -171,7 +170,6 @@ import android.util.proto.ProtoOutputStream; import android.view.DisplayInfo; import android.view.InsetsState; import android.view.RemoteAnimationAdapter; -import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.WindowManager.TransitionOldType; @@ -362,6 +360,10 @@ class Task extends TaskFragment { * user wants to return to it. */ private WindowProcessController mRootProcess; + /** The TF host info are set once the task has ever added an organized task fragment. */ + int mTaskFragmentHostUid; + String mTaskFragmentHostProcessName; + /** Takes on same value as first root activity */ boolean isPersistable = false; int maxRecents; @@ -438,16 +440,6 @@ class Task extends TaskFragment { // Id of the previous display the root task was on. int mPrevDisplayId = INVALID_DISPLAY; - /** ID of the display which rotation {@link #mRotation} has. */ - private int mLastRotationDisplayId = INVALID_DISPLAY; - - /** - * Display rotation as of the last time {@link #setBounds(Rect)} was called or this task was - * moved to a new display. - */ - @Surface.Rotation - private int mRotation; - int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE; /** @@ -458,10 +450,7 @@ class Task extends TaskFragment { */ int mLastReportedRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; - // For comparison with DisplayContent bounds. - private Rect mTmpRect = new Rect(); - // For handling display rotations. - private Rect mTmpRect2 = new Rect(); + private final Rect mTmpRect = new Rect(); // Resize mode of the task. See {@link ActivityInfo#resizeMode} // Based on the {@link ActivityInfo#resizeMode} of the root activity. @@ -1194,9 +1183,6 @@ class Task extends TaskFragment { updateOverrideConfigurationFromLaunchBounds(); } - // Update task bounds if needed. - adjustBoundsForDisplayChangeIfNeeded(getDisplayContent()); - mRootWindowContainer.updateUIDsPresentOnDisplay(); // Ensure all animations are finished at same time in split-screen mode. @@ -1468,6 +1454,11 @@ class Task extends TaskFragment { // passed from Task constructor. final TaskFragment childTaskFrag = child.asTaskFragment(); if (childTaskFrag != null && childTaskFrag.asTask() == null) { + if (childTaskFrag.mTaskFragmentOrganizerProcessName != null + && mTaskFragmentHostProcessName == null) { + mTaskFragmentHostUid = childTaskFrag.mTaskFragmentOrganizerUid; + mTaskFragmentHostProcessName = childTaskFrag.mTaskFragmentOrganizerProcessName; + } childTaskFrag.setMinDimensions(mMinWidth, mMinHeight); // The starting window should keep covering its task when a pure TaskFragment is added @@ -2731,15 +2722,7 @@ class Task extends TaskFragment { return setBounds(getRequestedOverrideBounds(), bounds); } - int rotation = Surface.ROTATION_0; - final DisplayContent displayContent = getRootTask() != null - ? getRootTask().getDisplayContent() : null; - if (displayContent != null) { - rotation = displayContent.getDisplayInfo().rotation; - } - final int boundsChange = super.setBounds(bounds); - mRotation = rotation; updateSurfacePositionNonOrganized(); return boundsChange; } @@ -2799,10 +2782,6 @@ class Task extends TaskFragment { @Override void onDisplayChanged(DisplayContent dc) { - final boolean isRootTask = isRootTask(); - if (!isRootTask && !mCreatedByOrganizer) { - adjustBoundsForDisplayChangeIfNeeded(dc); - } super.onDisplayChanged(dc); if (isLeafTask()) { final int displayId = (dc != null) ? dc.getDisplayId() : INVALID_DISPLAY; @@ -2953,48 +2932,6 @@ class Task extends TaskFragment { return mDragResizing; } - void adjustBoundsForDisplayChangeIfNeeded(final DisplayContent displayContent) { - if (displayContent == null) { - return; - } - if (getRequestedOverrideBounds().isEmpty()) { - return; - } - final int displayId = displayContent.getDisplayId(); - final int newRotation = displayContent.getDisplayInfo().rotation; - if (displayId != mLastRotationDisplayId) { - // This task is on a display that it wasn't on. There is no point to keep the relative - // position if display rotations for old and new displays are different. Just keep these - // values. - mLastRotationDisplayId = displayId; - mRotation = newRotation; - return; - } - - if (mRotation == newRotation) { - // Rotation didn't change. We don't need to adjust the bounds to keep the relative - // position. - return; - } - - // Device rotation changed. - // - We don't want the task to move around on the screen when this happens, so update the - // task bounds so it stays in the same place. - // - Rotate the bounds and notify activity manager if the task can be resized independently - // from its root task. The root task will take care of task rotation for the other case. - mTmpRect2.set(getBounds()); - - if (!getWindowConfiguration().canResizeTask()) { - setBounds(mTmpRect2); - return; - } - - displayContent.rotateBounds(mRotation, newRotation, mTmpRect2); - if (setBounds(mTmpRect2) != BOUNDS_CHANGE_NONE) { - mAtmService.resizeTask(mTaskId, getBounds(), RESIZE_MODE_SYSTEM_SCREEN_ROTATION); - } - } - /** Cancels any running app transitions associated with the task. */ void cancelTaskWindowTransition() { for (int i = mChildren.size() - 1; i >= 0; --i) { @@ -3525,8 +3462,6 @@ class Task extends TaskFragment { info.isVisibleRequested = isVisibleRequested(); info.isSleeping = shouldSleepActivities(); info.isTopActivityTransparent = top != null && !top.fillsParent(); - appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null - && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled(); appCompatTaskInfo.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; appCompatTaskInfo.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; appCompatTaskInfo.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; @@ -3541,15 +3476,29 @@ class Task extends TaskFragment { appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width(); appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height(); } + // We need to consider if letterboxed or pillarboxed + // TODO(b/336807329) Encapsulate reachability logic + appCompatTaskInfo.isLetterboxDoubleTapEnabled = top != null + && top.mLetterboxUiController.isLetterboxDoubleTapEducationEnabled(); if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { if (appCompatTaskInfo.isTopActivityPillarboxed()) { - // Pillarboxed - appCompatTaskInfo.topActivityLetterboxHorizontalPosition = - top.mLetterboxUiController.getLetterboxPositionForHorizontalReachability(); + if (top.mLetterboxUiController.allowHorizontalReachabilityForThinLetterbox()) { + // Pillarboxed + appCompatTaskInfo.topActivityLetterboxHorizontalPosition = + top.mLetterboxUiController + .getLetterboxPositionForHorizontalReachability(); + } else { + appCompatTaskInfo.isLetterboxDoubleTapEnabled = false; + } } else { - // Letterboxed - appCompatTaskInfo.topActivityLetterboxVerticalPosition = - top.mLetterboxUiController.getLetterboxPositionForVerticalReachability(); + if (top.mLetterboxUiController.allowVerticalReachabilityForThinLetterbox()) { + // Letterboxed + appCompatTaskInfo.topActivityLetterboxVerticalPosition = + top.mLetterboxUiController + .getLetterboxPositionForVerticalReachability(); + } else { + appCompatTaskInfo.isLetterboxDoubleTapEnabled = false; + } } } appCompatTaskInfo.topActivityEligibleForUserAspectRatioButton = top != null diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 2b631f7a404e..6a7f60b3447d 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -320,9 +320,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** Organizer that organizing this TaskFragment. */ @Nullable private ITaskFragmentOrganizer mTaskFragmentOrganizer; - @VisibleForTesting + int mTaskFragmentOrganizerUid = INVALID_UID; - private @Nullable String mTaskFragmentOrganizerProcessName; + @Nullable String mTaskFragmentOrganizerProcessName; /** Client assigned unique token for this TaskFragment if this is created by an organizer. */ @Nullable @@ -485,14 +485,16 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ @Nullable private WindowProcessController getOrganizerProcessIfDifferent(@Nullable ActivityRecord r) { - if ((r == null || mTaskFragmentOrganizerProcessName == null) - || (mTaskFragmentOrganizerProcessName.equals(r.processName) - && mTaskFragmentOrganizerUid == r.getUid())) { - // No organizer or the process is the same. + final Task task = getTask(); + if (r == null || task == null || task.mTaskFragmentHostProcessName == null) { + return null; + } + if (task.mTaskFragmentHostProcessName.equals(r.processName) + && task.mTaskFragmentHostUid == r.getUid()) { return null; } - return mAtmService.getProcessController(mTaskFragmentOrganizerProcessName, - mTaskFragmentOrganizerUid); + return mAtmService.getProcessController(task.mTaskFragmentHostProcessName, + task.mTaskFragmentHostUid); } void setAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index c25080f9e756..e90c845f0d21 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -858,6 +858,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * {@link InsetsStateController#notifyInsetsChanged}. */ boolean isReadyToDispatchInsetsState() { + if (mStartingData != null) { + // Starting window doesn't consider insets. + return false; + } final boolean visible = shouldCheckTokenVisibleRequested() ? isVisibleRequested() : isVisible(); return visible && mFrozenInsetsState == null; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 32cb2510bc9f..62f5b89e701c 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -88,7 +88,6 @@ namespace input_flags = com::android::input::flags; namespace android { -static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer(); static const bool ENABLE_INPUT_FILTER_RUST = input_flags::enable_input_filter_rust_impl(); // The exponent used to calculate the pointer speed scaling factor. @@ -298,10 +297,8 @@ public: void setShowTouches(bool enabled); void setInteractive(bool interactive); void reloadCalibration(); - void setPointerIconType(PointerIconStyle iconId); void reloadPointerIcons(); void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled); - void setCustomPointerIcon(const SpriteIcon& icon); bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId, DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken); @@ -316,7 +313,6 @@ public: /* --- InputReaderPolicyInterface implementation --- */ void getReaderConfiguration(InputReaderConfiguration* outConfig) override; - std::shared_ptr<PointerControllerInterface> obtainPointerController(int32_t deviceId) override; void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override; std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay( const InputDeviceIdentifier& identifier, @@ -375,7 +371,6 @@ public: virtual PointerIconStyle getDefaultPointerIconId(); virtual PointerIconStyle getDefaultStylusIconId(); virtual PointerIconStyle getCustomPointerIconId(); - virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position); /* --- PointerChoreographerPolicyInterface implementation --- */ std::shared_ptr<PointerControllerInterface> createPointerController( @@ -409,19 +404,12 @@ private: // True if pointer gestures are enabled. bool pointerGesturesEnabled{true}; - // Show touches feature enable/disable. - bool showTouches{false}; - // The latest request to enable or disable Pointer Capture. PointerCaptureRequest pointerCaptureRequest{}; // Sprite controller singleton, created on first use. std::shared_ptr<SpriteController> spriteController{}; - // TODO(b/293587049): Remove when the PointerChoreographer refactoring is complete. - // Pointer controller singleton, created and destroyed as needed. - std::weak_ptr<PointerController> legacyPointerController{}; - // The list of PointerControllers created and managed by the PointerChoreographer. std::list<std::weak_ptr<PointerController>> pointerControllers{}; @@ -504,14 +492,10 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n", dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", - toString(mLocked.pointerGesturesEnabled)); - dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches)); + toString(mLocked.pointerGesturesEnabled)); dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n", mLocked.pointerCaptureRequest.isEnable() ? "Enabled" : "Disabled", mLocked.pointerCaptureRequest.seq); - if (auto pc = mLocked.legacyPointerController.lock(); pc) { - dump += pc->dump(); - } } // release lock dump += "\n"; @@ -556,9 +540,7 @@ void NativeInputManager::setDisplayViewports(JNIEnv* env, jobjectArray viewportO [&viewports](PointerController& pc) { pc.onDisplayViewportsUpdated(viewports); }); } // release lock - if (ENABLE_POINTER_CHOREOGRAPHER) { - mInputManager->getChoreographer().setDisplayViewports(viewports); - } + mInputManager->getChoreographer().setDisplayViewports(viewports); mInputManager->getReader().requestRefreshConfiguration( InputReaderConfiguration::Change::DISPLAY_INFO); } @@ -700,8 +682,6 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon : 1; outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled; - outConfig->showTouches = mLocked.showTouches; - outConfig->pointerCaptureRequest = mLocked.pointerCaptureRequest; outConfig->setDisplayViewports(mLocked.viewports); @@ -743,10 +723,6 @@ std::unordered_map<std::string, T> NativeInputManager::readMapFromInterleavedJav void NativeInputManager::forEachPointerControllerLocked( std::function<void(PointerController&)> apply) { - if (auto pc = mLocked.legacyPointerController.lock(); pc) { - apply(*pc); - } - auto it = mLocked.pointerControllers.begin(); while (it != mLocked.pointerControllers.end()) { auto pc = it->lock(); @@ -780,50 +756,16 @@ PointerIcon NativeInputManager::loadPointerIcon(JNIEnv* env, int32_t displayId, return android_view_PointerIcon_toNative(env, pointerIconObj.get()); } -// TODO(b/293587049): Remove the old way of obtaining PointerController when the -// PointerChoreographer refactoring is complete. -std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController( - int32_t /* deviceId */) { - ATRACE_CALL(); - std::scoped_lock _l(mLock); - - std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock(); - if (controller == nullptr) { - ensureSpriteControllerLocked(); - - // Disable the functionality of the legacy PointerController if PointerChoreographer is - // enabled. - controller = PointerController::create(this, mLooper, *mLocked.spriteController, - /*enabled=*/!ENABLE_POINTER_CHOREOGRAPHER); - mLocked.legacyPointerController = controller; - updateInactivityTimeoutLocked(); - } - - return controller; -} - std::shared_ptr<PointerControllerInterface> NativeInputManager::createPointerController( PointerControllerInterface::ControllerType type) { std::scoped_lock _l(mLock); ensureSpriteControllerLocked(); std::shared_ptr<PointerController> pc = - PointerController::create(this, mLooper, *mLocked.spriteController, /*enabled=*/true, - type); + PointerController::create(this, mLooper, *mLocked.spriteController, type); mLocked.pointerControllers.emplace_back(pc); return pc; } -void NativeInputManager::onPointerDisplayIdChanged(int32_t pointerDisplayId, - const FloatPoint& position) { - if (ENABLE_POINTER_CHOREOGRAPHER) { - return; - } - JNIEnv* env = jniEnv(); - env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId, - position.x, position.y); - checkAndClearExceptionFromCallback(env, "onPointerDisplayIdChanged"); -} - void NativeInputManager::notifyPointerDisplayIdChanged(int32_t pointerDisplayId, const FloatPoint& position) { // Notify the Reader so that devices can be reconfigured. @@ -1210,23 +1152,7 @@ void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) { } void NativeInputManager::setPointerDisplayId(int32_t displayId) { - if (ENABLE_POINTER_CHOREOGRAPHER) { - mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId); - } else { - { // acquire lock - std::scoped_lock _l(mLock); - - if (mLocked.pointerDisplayId == displayId) { - return; - } - - ALOGI("Setting pointer display id to %d.", displayId); - mLocked.pointerDisplayId = displayId; - } // release lock - - mInputManager->getReader().requestRefreshConfiguration( - InputReaderConfiguration::Change::DISPLAY_INFO); - } + mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId); } int32_t NativeInputManager::getMousePointerSpeed() { @@ -1378,24 +1304,7 @@ void NativeInputManager::setInputDeviceEnabled(uint32_t deviceId, bool enabled) } void NativeInputManager::setShowTouches(bool enabled) { - if (ENABLE_POINTER_CHOREOGRAPHER) { - mInputManager->getChoreographer().setShowTouchesEnabled(enabled); - return; - } - - { // acquire lock - std::scoped_lock _l(mLock); - - if (mLocked.showTouches == enabled) { - return; - } - - ALOGI("Setting show touches feature to %s.", enabled ? "enabled" : "disabled"); - mLocked.showTouches = enabled; - } // release lock - - mInputManager->getReader().requestRefreshConfiguration( - InputReaderConfiguration::Change::SHOW_TOUCHES); + mInputManager->getChoreographer().setShowTouchesEnabled(enabled); } void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, bool enabled) { @@ -1411,27 +1320,11 @@ void NativeInputManager::reloadCalibration() { InputReaderConfiguration::Change::TOUCH_AFFINE_TRANSFORMATION); } -void NativeInputManager::setPointerIconType(PointerIconStyle iconId) { - std::scoped_lock _l(mLock); - std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock(); - if (controller != nullptr) { - controller->updatePointerIcon(iconId); - } -} - void NativeInputManager::reloadPointerIcons() { std::scoped_lock _l(mLock); forEachPointerControllerLocked([](PointerController& pc) { pc.reloadPointerResources(); }); } -void NativeInputManager::setCustomPointerIcon(const SpriteIcon& icon) { - std::scoped_lock _l(mLock); - std::shared_ptr<PointerController> controller = mLocked.legacyPointerController.lock(); - if (controller != nullptr) { - controller->setCustomPointerIcon(icon); - } -} - bool NativeInputManager::setPointerIcon( std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId, DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken) { @@ -1447,9 +1340,6 @@ bool NativeInputManager::setPointerIcon( } void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) { - if (!ENABLE_POINTER_CHOREOGRAPHER) { - return; - } mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible); } @@ -1819,36 +1709,12 @@ void NativeInputManager::setStylusButtonMotionEventsEnabled(bool enabled) { } FloatPoint NativeInputManager::getMouseCursorPosition(int32_t displayId) { - if (ENABLE_POINTER_CHOREOGRAPHER) { - return mInputManager->getChoreographer().getMouseCursorPosition(displayId); - } - // To maintain the status-quo, the displayId parameter (used when PointerChoreographer is - // enabled) is ignored in the old pipeline. - std::scoped_lock _l(mLock); - const auto pc = mLocked.legacyPointerController.lock(); - if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION}; - - return pc->getPosition(); + return mInputManager->getChoreographer().getMouseCursorPosition(displayId); } void NativeInputManager::setStylusPointerIconEnabled(bool enabled) { - if (ENABLE_POINTER_CHOREOGRAPHER) { - mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled); - return; - } - - { // acquire lock - std::scoped_lock _l(mLock); - - if (mLocked.stylusPointerIconEnabled == enabled) { - return; - } - - mLocked.stylusPointerIconEnabled = enabled; - } // release lock - - mInputManager->getReader().requestRefreshConfiguration( - InputReaderConfiguration::Change::DISPLAY_INFO); + mInputManager->getChoreographer().setStylusPointerIconEnabled(enabled); + return; } void NativeInputManager::setInputMethodConnectionIsActive(bool isActive) { @@ -2597,27 +2463,12 @@ static void nativeDisableInputDevice(JNIEnv* env, jobject nativeImplObj, jint de im->setInputDeviceEnabled(deviceId, false); } -static void nativeSetPointerIconType(JNIEnv* env, jobject nativeImplObj, jint iconId) { - // iconId is set in java from from frameworks/base/core/java/android/view/PointerIcon.java, - // where the definition in <input/Input.h> is duplicated as a sealed class (type safe enum - // equivalent in Java). - - NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - - im->setPointerIconType(static_cast<PointerIconStyle>(iconId)); -} - static void nativeReloadPointerIcons(JNIEnv* env, jobject nativeImplObj) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); im->reloadPointerIcons(); } -static void nativeSetCustomPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj) { - NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setCustomPointerIcon(toSpriteIcon(android_view_PointerIcon_toNative(env, iconObj))); -} - static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj, jint displayId, jint deviceId, jint pointerId, jobject inputTokenObj) { @@ -2937,10 +2788,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"isInputDeviceEnabled", "(I)Z", (void*)nativeIsInputDeviceEnabled}, {"enableInputDevice", "(I)V", (void*)nativeEnableInputDevice}, {"disableInputDevice", "(I)V", (void*)nativeDisableInputDevice}, - {"setPointerIconType", "(I)V", (void*)nativeSetPointerIconType}, {"reloadPointerIcons", "()V", (void*)nativeReloadPointerIcons}, - {"setCustomPointerIcon", "(Landroid/view/PointerIcon;)V", - (void*)nativeSetCustomPointerIcon}, {"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z", (void*)nativeSetPointerIcon}, {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility}, diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 8755a8077ddb..cfe4e17eb1be 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -47,6 +47,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.res.Configuration; import android.content.res.Resources.Theme; +import android.crashrecovery.flags.Flags; import android.credentials.CredentialManager; import android.database.sqlite.SQLiteCompatibilityWalFlags; import android.database.sqlite.SQLiteGlobal; @@ -1195,11 +1196,13 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class); t.traceEnd(); - // Now that we have the bare essentials of the OS up and running, take - // note that we just booted, which might send out a rescue party if - // we're stuck in a runtime restart loop. - RescueParty.registerHealthObserver(mSystemContext); - PackageWatchdog.getInstance(mSystemContext).noteBoot(); + if (!Flags.recoverabilityDetection()) { + // Now that we have the bare essentials of the OS up and running, take + // note that we just booted, which might send out a rescue party if + // we're stuck in a runtime restart loop. + RescueParty.registerHealthObserver(mSystemContext); + PackageWatchdog.getInstance(mSystemContext).noteBoot(); + } // Manages LEDs and display backlight so we need it to bring up the display. t.traceBegin("StartLightsService"); @@ -1469,9 +1472,12 @@ public final class SystemServer implements Dumpable { boolean enableVrService = context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); - // For debugging RescueParty - if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.crash_system", false)) { - throw new RuntimeException(); + if (!Flags.recoverabilityDetection()) { + // For debugging RescueParty + if (Build.IS_DEBUGGABLE + && SystemProperties.getBoolean("debug.crash_system", false)) { + throw new RuntimeException(); + } } try { @@ -2910,6 +2916,14 @@ public final class SystemServer implements Dumpable { mPackageManagerService.systemReady(); t.traceEnd(); + if (Flags.recoverabilityDetection()) { + // Now that we have the essential services needed for rescue party, initialize + // RescuParty. note that we just booted, which might send out a rescue party if + // we're stuck in a runtime restart loop. + RescueParty.registerHealthObserver(mSystemContext); + PackageWatchdog.getInstance(mSystemContext).noteBoot(); + } + t.traceBegin("MakeDisplayManagerServiceReady"); try { // TODO: use boot phase and communicate this flag some other way @@ -3313,6 +3327,14 @@ public final class SystemServer implements Dumpable { * are updated outside of OTA; and to avoid breaking dependencies from system into apexes. */ private void startApexServices(@NonNull TimingsTraceAndSlog t) { + if (Flags.recoverabilityDetection()) { + // For debugging RescueParty + if (Build.IS_DEBUGGABLE + && SystemProperties.getBoolean("debug.crash_system", false)) { + throw new RuntimeException(); + } + } + t.traceBegin("startApexServices"); // TODO(b/192880996): get the list from "android" package, once the manifest entries // are migrated to system manifest. diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java index 4a2164582890..42814e7c775e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java @@ -239,6 +239,9 @@ public class RescuePartyTest { @Test public void testBootLoopDetectionWithExecutionForAllRescueLevels() { + // this is old test where the flag needs to be disabled + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), any(Executor.class), @@ -449,6 +452,9 @@ public class RescuePartyTest { @Test public void testNonPersistentAppCrashDetectionWithScopedResets() { + // this is old test where the flag needs to be disabled + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), any(Executor.class), @@ -506,6 +512,9 @@ public class RescuePartyTest { @Test public void testNonDeviceConfigSettingsOnlyResetOncePerLevel() { + // this is old test where the flag needs to be disabled + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), any(Executor.class), @@ -879,6 +888,9 @@ public class RescuePartyTest { @Test public void testBootLoopLevels() { + // this is old test where the flag needs to be disabled + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); assertEquals(observer.onBootLoop(0), PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index 11f20e35b4b1..d15c24bd68b9 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -31,6 +31,7 @@ import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.JobSchedulerService.sUptimeMillisClock; import static com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS; import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK; +import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -74,6 +75,9 @@ import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import com.android.server.AppStateTracker; @@ -85,6 +89,8 @@ import com.android.server.SystemServiceManager; import com.android.server.job.controllers.ConnectivityController; import com.android.server.job.controllers.JobStatus; import com.android.server.job.controllers.QuotaController; +import com.android.server.job.restrictions.JobRestriction; +import com.android.server.job.restrictions.ThermalStatusRestriction; import com.android.server.pm.UserManagerInternal; import com.android.server.usage.AppStandbyInternal; @@ -121,6 +127,9 @@ public class JobSchedulerServiceTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private ChargingPolicyChangeListener mChargingPolicyChangeListener; private class TestJobSchedulerService extends JobSchedulerService { @@ -2385,6 +2394,108 @@ public class JobSchedulerServiceTest { assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b)); } + /** + * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with single {@link + * JobRestriction} registered. + */ + @Test + public void testCheckIfRestrictedSingleRestriction() { + int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE; + JobStatus fgsJob = + createJobStatus( + "testCheckIfRestrictedSingleRestriction", createJobInfo(1).setBias(bias)); + ThermalStatusRestriction mockThermalStatusRestriction = + mock(ThermalStatusRestriction.class); + mService.mJobRestrictions.clear(); + mService.mJobRestrictions.add(mockThermalStatusRestriction); + when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true); + + synchronized (mService.mLock) { + assertEquals(mService.checkIfRestricted(fgsJob), mockThermalStatusRestriction); + } + + when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false); + synchronized (mService.mLock) { + assertNull(mService.checkIfRestricted(fgsJob)); + } + } + + /** + * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with multiple {@link + * JobRestriction} registered. + */ + @Test + public void testCheckIfRestrictedMultipleRestrictions() { + int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE; + JobStatus fgsJob = + createJobStatus( + "testGetMinJobExecutionGuaranteeMs", createJobInfo(1).setBias(bias)); + JobRestriction mock1JobRestriction = mock(JobRestriction.class); + JobRestriction mock2JobRestriction = mock(JobRestriction.class); + mService.mJobRestrictions.clear(); + mService.mJobRestrictions.add(mock1JobRestriction); + mService.mJobRestrictions.add(mock2JobRestriction); + + // Jobs will be restricted if any one of the registered {@link JobRestriction} + // reports true. + when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true); + when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false); + synchronized (mService.mLock) { + assertEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction); + } + + when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false); + when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true); + synchronized (mService.mLock) { + assertEquals(mService.checkIfRestricted(fgsJob), mock2JobRestriction); + } + + when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false); + when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false); + synchronized (mService.mLock) { + assertNull(mService.checkIfRestricted(fgsJob)); + } + + when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true); + when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true); + synchronized (mService.mLock) { + assertNotEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction); + } + } + + /** + * Jobs with foreground service and top app biases must not be restricted when the flag is + * disabled. + */ + @Test + @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS) + public void testCheckIfRestricted_highJobBias_flagThermalRestrictionsToFgsJobsDisabled() { + JobStatus fgsJob = + createJobStatus( + "testCheckIfRestrictedJobBiasFgs", + createJobInfo(1).setBias(JobInfo.BIAS_FOREGROUND_SERVICE)); + JobStatus topAppJob = + createJobStatus( + "testCheckIfRestrictedJobBiasTopApp", + createJobInfo(2).setBias(JobInfo.BIAS_TOP_APP)); + + synchronized (mService.mLock) { + assertNull(mService.checkIfRestricted(fgsJob)); + assertNull(mService.checkIfRestricted(topAppJob)); + } + } + + /** Jobs with top app biases must not be restricted. */ + @Test + public void testCheckIfRestricted_highJobBias() { + JobStatus topAppJob = createJobStatus( + "testCheckIfRestrictedJobBiasTopApp", + createJobInfo(1).setBias(JobInfo.BIAS_TOP_APP)); + synchronized (mService.mLock) { + assertNull(mService.checkIfRestricted(topAppJob)); + } + } + private void setBatteryLevel(int level) { doReturn(level).when(mBatteryManagerInternal).getBatteryLevel(); mService.mBatteryStateTracker diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java index 754f409b3966..c2c67e615228 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java @@ -28,6 +28,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -43,7 +44,12 @@ import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; import android.os.PowerManager; +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.provider.DeviceConfig; +import android.util.DebugUtils; import androidx.test.runner.AndroidJUnit4; @@ -53,6 +59,7 @@ import com.android.server.job.controllers.JobStatus; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -76,6 +83,157 @@ public class ThermalStatusRestrictionTest { @Mock private JobSchedulerService mJobSchedulerService; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + class JobStatusContainer { + public final JobStatus jobMinPriority; + public final JobStatus jobLowPriority; + public final JobStatus jobLowPriorityRunning; + public final JobStatus jobLowPriorityRunningLong; + public final JobStatus jobDefaultPriority; + public final JobStatus jobHighPriority; + public final JobStatus jobHighPriorityRunning; + public final JobStatus jobHighPriorityRunningLong; + public final JobStatus ejDowngraded; + public final JobStatus ej; + public final JobStatus ejRetried; + public final JobStatus ejRunning; + public final JobStatus ejRunningLong; + public final JobStatus ui; + public final JobStatus uiRetried; + public final JobStatus uiRunning; + public final JobStatus uiRunningLong; + public final JobStatus importantWhileForeground; + public final JobStatus importantWhileForegroundRunning; + public final JobStatus importantWhileForegroundRunningLong; + public final int[] allJobBiases = { + JobInfo.BIAS_ADJ_ALWAYS_RUNNING, + JobInfo.BIAS_ADJ_OFTEN_RUNNING, + JobInfo.BIAS_DEFAULT, + JobInfo.BIAS_SYNC_EXPEDITED, + JobInfo.BIAS_SYNC_INITIALIZATION, + JobInfo.BIAS_BOUND_FOREGROUND_SERVICE, + JobInfo.BIAS_FOREGROUND_SERVICE, + JobInfo.BIAS_TOP_APP + }; + public final int[] biasesBelowFgs = { + JobInfo.BIAS_ADJ_ALWAYS_RUNNING, + JobInfo.BIAS_ADJ_OFTEN_RUNNING, + JobInfo.BIAS_DEFAULT, + JobInfo.BIAS_SYNC_EXPEDITED, + JobInfo.BIAS_SYNC_INITIALIZATION, + JobInfo.BIAS_BOUND_FOREGROUND_SERVICE + }; + public final int[] thermalStatuses = { + THERMAL_STATUS_NONE, + THERMAL_STATUS_LIGHT, + THERMAL_STATUS_MODERATE, + THERMAL_STATUS_SEVERE, + THERMAL_STATUS_CRITICAL, + THERMAL_STATUS_EMERGENCY, + THERMAL_STATUS_SHUTDOWN + }; + + JobStatusContainer(String jobName, JobSchedulerService mJobSchedulerService) { + jobMinPriority = + createJobStatus( + jobName, createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build()); + jobLowPriority = + createJobStatus( + jobName, createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build()); + jobLowPriorityRunning = + createJobStatus( + jobName, createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build()); + jobLowPriorityRunningLong = + createJobStatus( + jobName, createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build()); + jobDefaultPriority = + createJobStatus( + jobName, + createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build()); + jobHighPriority = + createJobStatus( + jobName, + createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build()); + jobHighPriorityRunning = + createJobStatus( + jobName, + createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build()); + jobHighPriorityRunningLong = + createJobStatus( + jobName, + createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build()); + ejDowngraded = createJobStatus(jobName, createJobBuilder(7).setExpedited(true).build()); + ej = spy(createJobStatus(jobName, createJobBuilder(8).setExpedited(true).build())); + ejRetried = + spy(createJobStatus(jobName, createJobBuilder(11).setExpedited(true).build())); + ejRunning = + spy(createJobStatus(jobName, createJobBuilder(12).setExpedited(true).build())); + ejRunningLong = + spy(createJobStatus(jobName, createJobBuilder(13).setExpedited(true).build())); + ui = spy(createJobStatus(jobName, createJobBuilder(14).build())); + uiRetried = spy(createJobStatus(jobName, createJobBuilder(15).build())); + uiRunning = spy(createJobStatus(jobName, createJobBuilder(16).build())); + uiRunningLong = spy(createJobStatus(jobName, createJobBuilder(17).build())); + importantWhileForeground = spy(createJobStatus(jobName, createJobBuilder(18) + .setImportantWhileForeground(true) + .build())); + importantWhileForegroundRunning = spy(createJobStatus(jobName, createJobBuilder(20) + .setImportantWhileForeground(true) + .build())); + importantWhileForegroundRunningLong = spy(createJobStatus(jobName, createJobBuilder(19) + .setImportantWhileForeground(true) + .build())); + + when(ej.shouldTreatAsExpeditedJob()).thenReturn(true); + when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true); + when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true); + when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true); + when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true); + when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true); + when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true); + when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true); + when(ejRetried.getNumPreviousAttempts()).thenReturn(1); + when(uiRetried.getNumPreviousAttempts()).thenReturn(2); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)) + .thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning)) + .thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunning)) + .thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunningLong)) + .thenReturn(true); + when(mJobSchedulerService.isJobInOvertimeLocked(importantWhileForegroundRunningLong)) + .thenReturn(true); + } + } + + private boolean isJobRestricted(JobStatus status, int bias) { + return mThermalStatusRestriction.isJobRestricted(status, bias); + } + + private static String debugTag(int bias, @PowerManager.ThermalStatus int status) { + return "Bias = " + + JobInfo.getBiasString(bias) + + " Thermal Status = " + + DebugUtils.valueToString(PowerManager.class, "THERMAL_STATUS_", status); + } + @Before public void setUp() { mMockingSession = mockitoSession() @@ -156,169 +314,302 @@ public class ThermalStatusRestrictionTest { assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus()); } + /** + * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Thermal is in default state + */ @Test - public void testIsJobRestricted() { + public void testIsJobRestrictedDefaultStates() { mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE); + JobStatusContainer jc = new JobStatusContainer("testIsJobRestricted", mJobSchedulerService); + + for (int jobBias : jc.allJobBiases) { + assertFalse(isJobRestricted(jc.jobMinPriority, jobBias)); + assertFalse(isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias)); + assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertFalse(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + assertFalse(isJobRestricted(jc.ej, jobBias)); + assertFalse(isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(isJobRestricted(jc.ui, jobBias)); + assertFalse(isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(isJobRestricted(jc.uiRunningLong, jobBias)); + } + } - final JobStatus jobMinPriority = createJobStatus("testIsJobRestricted", - createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build()); - final JobStatus jobLowPriority = createJobStatus("testIsJobRestricted", - createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build()); - final JobStatus jobLowPriorityRunning = createJobStatus("testIsJobRestricted", - createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build()); - final JobStatus jobLowPriorityRunningLong = createJobStatus("testIsJobRestricted", - createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build()); - final JobStatus jobDefaultPriority = createJobStatus("testIsJobRestricted", - createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build()); - final JobStatus jobHighPriority = createJobStatus("testIsJobRestricted", - createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build()); - final JobStatus jobHighPriorityRunning = createJobStatus("testIsJobRestricted", - createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build()); - final JobStatus jobHighPriorityRunningLong = createJobStatus("testIsJobRestricted", - createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build()); - final JobStatus ejDowngraded = createJobStatus("testIsJobRestricted", - createJobBuilder(7).setExpedited(true).build()); - final JobStatus ej = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(8).setExpedited(true).build())); - final JobStatus ejRetried = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(11).setExpedited(true).build())); - final JobStatus ejRunning = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(12).setExpedited(true).build())); - final JobStatus ejRunningLong = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(13).setExpedited(true).build())); - final JobStatus ui = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(14).build())); - final JobStatus uiRetried = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(15).build())); - final JobStatus uiRunning = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(16).build())); - final JobStatus uiRunningLong = spy(createJobStatus("testIsJobRestricted", - createJobBuilder(17).build())); - when(ej.shouldTreatAsExpeditedJob()).thenReturn(true); - when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true); - when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true); - when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true); - when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true); - when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true); - when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true); - when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true); - when(ejRetried.getNumPreviousAttempts()).thenReturn(1); - when(uiRetried.getNumPreviousAttempts()).thenReturn(2); - when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning)) - .thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong)) - .thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong)) - .thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true); - when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true); - when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong)) - .thenReturn(true); - when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong)) - .thenReturn(true); - when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true); - when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true); - - assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ui)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong)); - - mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT); - - assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ui)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong)); - - mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE); - - assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); - assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong)); - assertFalse(mThermalStatusRestriction.isJobRestricted(ui)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning)); - assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong)); - - mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE); + /** + * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Top App and all + * Thermal states. + */ + @Test + public void testIsJobRestrictedBiasTopApp() { + JobStatusContainer jc = + new JobStatusContainer("testIsJobRestrictedBiasTopApp", mJobSchedulerService); + + int jobBias = JobInfo.BIAS_TOP_APP; + for (int thermalStatus : jc.thermalStatuses) { + String msg = "Thermal Status = " + DebugUtils.valueToString( + PowerManager.class, "THERMAL_STATUS_", thermalStatus); + mStatusChangedListener.onThermalStatusChanged(thermalStatus); + + // No restrictions on any jobs + assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } + } - assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ej)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ui)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong)); + /** + * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground + * Service and all Thermal states. + */ + @Test + @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS) + public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsDisabled() { + JobStatusContainer jc = + new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService); + + int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE; + for (int thermalStatus : jc.thermalStatuses) { + String msg = "Thermal Status = " + DebugUtils.valueToString( + PowerManager.class, "THERMAL_STATUS_", thermalStatus); + mStatusChangedListener.onThermalStatusChanged(thermalStatus); + // No restrictions on any jobs + assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } + } - mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL); + /** + * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground + * Service and all Thermal states. + */ + @Test + @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS) + public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled() { + JobStatusContainer jc = + new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService); + int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE; + for (int thermalStatus : jc.thermalStatuses) { + String msg = debugTag(jobBias, thermalStatus); + mStatusChangedListener.onThermalStatusChanged(thermalStatus); + if (thermalStatus >= THERMAL_STATUS_SEVERE) { + // Full restrictions on all jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ui, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } else if (thermalStatus >= THERMAL_STATUS_MODERATE) { + // No restrictions on user related jobs + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + // Some restrictions on expedited jobs + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + // Some restrictions on high priority jobs + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + // Some restructions on important while foreground jobs + assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias)); + assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertTrue(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + // Full restriction on default priority jobs + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + // Full restriction on low priority jobs + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + // Full restriction on min priority jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } else { + // thermalStatus < THERMAL_STATUS_MODERATE + // No restrictions on any job type + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } + } + } - assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ej)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong)); - assertTrue(mThermalStatusRestriction.isJobRestricted(ui)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning)); - assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong)); + /** + * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is less than + * Foreground Service and all Thermal states. + */ + @Test + public void testIsJobRestrictedBiasLessThanFgs() { + JobStatusContainer jc = + new JobStatusContainer("testIsJobRestrictedBiasLessThanFgs", mJobSchedulerService); + + for (int jobBias : jc.biasesBelowFgs) { + for (int thermalStatus : jc.thermalStatuses) { + String msg = debugTag(jobBias, thermalStatus); + mStatusChangedListener.onThermalStatusChanged(thermalStatus); + if (thermalStatus >= THERMAL_STATUS_SEVERE) { + // Full restrictions on all jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ui, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } else if (thermalStatus >= THERMAL_STATUS_MODERATE) { + // No restrictions on user related jobs + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + // Some restrictions on expedited jobs + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + // Some restrictions on high priority jobs + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + // Full restriction on default priority jobs + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + // Full restriction on low priority jobs + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + // Full restriction on min priority jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } else if (thermalStatus >= THERMAL_STATUS_LIGHT) { + // No restrictions on any user related jobs + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + // No restrictions on any expedited jobs + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + // No restrictions on any high priority jobs + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + // No restrictions on default priority jobs + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + // Some restrictions on low priority jobs + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + // Full restriction on min priority jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } else { // THERMAL_STATUS_NONE + // No restrictions on any jobs + assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } + } + } } private JobInfo.Builder createJobBuilder(int jobId) { diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 79f1574105ba..7d58a2e53693 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -778,6 +778,49 @@ public final class UserManagerServiceTest { } @Test + public void testGetAliveUsers_shouldExcludeInitialisedEphemeralNonCurrentUsers() { + assertWithMessage("Ephemeral user should not exist at all initially") + .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID)) + .isFalse(); + + // add an ephemeral full user + TestUserData userData = new TestUserData(USER_ID); + userData.info.flags = UserInfo.FLAG_FULL | UserInfo.FLAG_EPHEMERAL; + addUserData(userData); + + assertWithMessage("Ephemeral user should exist as alive after being created") + .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID)) + .isTrue(); + + // mock switch to the user (mark it as initialized & make it the current user) + userData.info.flags |= UserInfo.FLAG_INITIALIZED; + mockCurrentUser(USER_ID); + + assertWithMessage("Ephemeral user should still exist as alive after being switched to") + .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID)) + .isTrue(); + + // switch away from the user + mockCurrentUser(OTHER_USER_ID); + + assertWithMessage("Ephemeral user should not exist as alive after getting switched away") + .that(mUmi.getUsers(true).stream().anyMatch(u -> u.id == USER_ID)) + .isFalse(); + + assertWithMessage("Ephemeral user should still exist as dying after getting switched away") + .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID)) + .isTrue(); + + // finally remove the user + mUms.removeUserInfo(USER_ID); + + assertWithMessage("Ephemeral user should not exist at all after cleanup") + .that(mUmi.getUsers(false).stream().anyMatch(u -> u.id == USER_ID)) + .isFalse(); + } + + + @Test @RequiresFlagsEnabled({android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, Flags.FLAG_BLOCK_PRIVATE_SPACE_CREATION, Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES}) public void testCreatePrivateProfileOnHeadlessSystemUser_shouldAllowCreation() { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java new file mode 100644 index 000000000000..0716a5c5561d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java @@ -0,0 +1,132 @@ +/* + * 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.biometrics; + +import static com.android.server.biometrics.sensors.BiometricNotificationUtils.NOTIFICATION_ID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationManager; +import android.content.Intent; +import android.hardware.biometrics.BiometricsProtoEnums; +import android.os.UserHandle; +import android.provider.Settings; +import android.testing.TestableContext; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BiometricDanglingReceiverTest { + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + private BiometricDanglingReceiver mBiometricDanglingReceiver; + + @Rule + public final TestableContext mContext = spy(new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext(), null)); + + @Mock + NotificationManager mNotificationManager; + + @Mock + Intent mIntent; + + @Captor + private ArgumentCaptor<Intent> mArgumentCaptor; + + @Before + public void setUp() { + mContext.addMockSystemService(NotificationManager.class, mNotificationManager); + } + + @Test + public void testFingerprintRegisterReceiver() { + initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT); + verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any()); + } + + @Test + public void testFaceRegisterReceiver() { + initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE); + verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any()); + } + + @Test + public void testOnReceive_fingerprintReEnrollLaunch() { + initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT); + when(mIntent.getAction()).thenReturn( + BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH); + + mBiometricDanglingReceiver.onReceive(mContext, mIntent); + + // Verify fingerprint enroll process is launched. + verify(mContext).startActivity(mArgumentCaptor.capture()); + assertThat(mArgumentCaptor.getValue().getAction()) + .isEqualTo(Settings.ACTION_FINGERPRINT_ENROLL); + + // Verify notification is canceled + verify(mNotificationManager).cancelAsUser("FingerprintReEnroll", NOTIFICATION_ID, + UserHandle.CURRENT); + + // Verify receiver is unregistered after receiving the broadcast + verify(mContext).unregisterReceiver(mBiometricDanglingReceiver); + } + + @Test + public void testOnReceive_faceReEnrollLaunch() { + initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE); + when(mIntent.getAction()).thenReturn( + BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH); + + mBiometricDanglingReceiver.onReceive(mContext, mIntent); + + // Verify face enroll process is launched. + verify(mContext).startActivity(mArgumentCaptor.capture()); + assertThat(mArgumentCaptor.getValue().getAction()) + .isEqualTo(BiometricDanglingReceiver.FACE_SETTINGS_ACTION); + + // Verify notification is canceled + verify(mNotificationManager).cancelAsUser("FaceReEnroll", NOTIFICATION_ID, + UserHandle.CURRENT); + + // Verify receiver is unregistered after receiving the broadcast. + verify(mContext).unregisterReceiver(mBiometricDanglingReceiver); + } + + private void initBroadcastReceiver(int modality) { + mBiometricDanglingReceiver = new BiometricDanglingReceiver(mContext, modality); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index fc573d243129..37895315557a 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -1228,6 +1228,11 @@ public class BiometricSchedulerTest { Slog.d(TAG, "TestInternalEnumerateClient#startHalOperation"); onEnumerationResult(TEST_FINGERPRINT, 0 /* remaining */); } + + @Override + protected int getModality() { + return BiometricsProtoEnums.MODALITY_FINGERPRINT; + } } private static class TestRemovalClient extends RemovalClient<Fingerprint, Object> { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java index 9845b58f79bf..d8bdd50d8c08 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java @@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -79,15 +81,21 @@ public class FaceInternalEnumerateClientTest { private final int mBiometricId = 1; private final Face mFace = new Face("face", mBiometricId, 1 /* deviceId */); private FaceInternalEnumerateClient mClient; + private boolean mNotificationSent; @Before public void setUp() { when(mAidlSession.getSession()).thenReturn(mSession); - final List<Face> enrolled = new ArrayList<>(); enrolled.add(mFace); - mClient = new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID, - TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext); + mClient = spy(new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID, + TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext)); + + mNotificationSent = false; + doAnswer(invocation -> { + mNotificationSent = true; + return null; + }).when(mClient).sendDanglingNotification(anyList()); } @Test @@ -101,6 +109,7 @@ public class FaceInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0); + assertThat(mNotificationSent).isFalse(); verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt()); verify(mCallback).onClientFinished(mClient, true); } @@ -116,6 +125,7 @@ public class FaceInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0); + assertThat(mNotificationSent).isFalse(); verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt()); verify(mCallback, never()).onClientFinished(mClient, true); } @@ -131,6 +141,7 @@ public class FaceInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0); + assertThat(mNotificationSent).isTrue(); verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId); verify(mCallback).onClientFinished(mClient, true); } @@ -147,6 +158,7 @@ public class FaceInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1); + assertThat(mNotificationSent).isFalse(); verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt()); verify(mCallback, never()).onClientFinished(mClient, true); } @@ -164,6 +176,7 @@ public class FaceInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1); + assertThat(mNotificationSent).isTrue(); verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId); verify(mCallback).onClientFinished(mClient, true); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java index b5df8362b09f..fab120016434 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java @@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -80,15 +82,23 @@ public class FingerprintInternalEnumerateClientTest { private FingerprintInternalEnumerateClient mClient; + private boolean mNotificationSent; + @Before public void setUp() { when(mAidlSession.getSession()).thenReturn(mSession); List<Fingerprint> enrolled = new ArrayList<>(); enrolled.add(new Fingerprint("one", 1, 1)); - mClient = new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken, + mClient = spy(new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID, TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, - mBiometricContext); + mBiometricContext)); + + mNotificationSent = false; + doAnswer(invocation -> { + mNotificationSent = true; + return null; + }).when(mClient).sendDanglingNotification(anyList()); } @Test @@ -104,6 +114,7 @@ public class FingerprintInternalEnumerateClientTest { assertThat(mClient.getUnknownHALTemplates().stream() .flatMap(x -> Stream.of(x.getBiometricId())) .collect(Collectors.toList())).containsExactly(2, 3); + assertThat(mNotificationSent).isTrue(); verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, 1); verify(mCallback).onClientFinished(mClient, true); } @@ -118,6 +129,7 @@ public class FingerprintInternalEnumerateClientTest { verify(mSession).enumerateEnrollments(); assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0); + assertThat(mNotificationSent).isFalse(); verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt()); verify(mCallback).onClientFinished(mClient, true); } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index fd880dd23f25..178e7ec649c6 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -33,7 +33,6 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.DisplayInfo; @@ -41,13 +40,11 @@ import android.view.WindowManager; import androidx.test.InstrumentationRegistry; -import com.android.input.flags.Flags; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -60,9 +57,6 @@ public class InputControllerTest { private static final String LANGUAGE_TAG = "en-US"; private static final String LAYOUT_TYPE = "qwerty"; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private InputManagerInternal mInputManagerInternalMock; @Mock @@ -77,8 +71,6 @@ public class InputControllerTest { @Before public void setUp() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); - MockitoAnnotations.initMocks(this); mInputManagerMockHelper = new InputManagerMockHelper( TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 2b81d78a9163..da8961df38a8 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -339,8 +339,6 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); - doNothing().when(mInputManagerInternalMock) .setMousePointerAccelerationEnabled(anyBoolean(), anyInt()); doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 5fdb3965be76..d1423fe71e50 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -3002,39 +3002,45 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); } - private enum ModesApiFlag { - ENABLED(true, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER), - DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI); + private enum ModesFlag { + MODES_UI(2, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER), + MODES_API(1, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_USER), + DISABLED(0, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI); - private final boolean mEnabled; + private final int mFlagsEnabled; @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi; - ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) { - this.mEnabled = enabled; + ModesFlag(int flagsEnabled, @ConfigChangeOrigin int originForUserActionInSystemUi) { + this.mFlagsEnabled = flagsEnabled; this.mOriginForUserActionInSystemUi = originForUserActionInSystemUi; } - void applyFlag(SetFlagsRule setFlagsRule) { - if (mEnabled) { + void applyFlags(SetFlagsRule setFlagsRule) { + if (mFlagsEnabled >= 1) { setFlagsRule.enableFlags(Flags.FLAG_MODES_API); } else { setFlagsRule.disableFlags(Flags.FLAG_MODES_API); } + if (mFlagsEnabled >= 2) { + setFlagsRule.enableFlags(Flags.FLAG_MODES_UI); + } else { + setFlagsRule.disableFlags(Flags.FLAG_MODES_UI); + } } } @Test - public void testZenModeEventLog_setManualZenMode(@TestParameter ModesApiFlag modesApiFlag) + public void testZenModeEventLog_setManualZenMode(@TestParameter ModesFlag modesFlag) throws IllegalArgumentException { - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode @@ -3062,7 +3068,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0)); assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertThat(mZenModeEventLogger.getFromSystemOrSystemUi(0)).isEqualTo( - modesApiFlag == ModesApiFlag.DISABLED); + modesFlag == ModesFlag.DISABLED); assertTrue(mZenModeEventLogger.getIsUserAction(0)); assertEquals(Process.SYSTEM_UID, mZenModeEventLogger.getPackageUid(0)); checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); @@ -3091,9 +3097,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_automaticRules(@TestParameter ModesApiFlag modesApiFlag) + public void testZenModeEventLog_automaticRules(@TestParameter ModesFlag modesFlag) throws IllegalArgumentException { - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3116,7 +3122,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, - modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); // Add a new system rule AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", @@ -3134,7 +3140,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Event 4: "User" deletes the rule - mZenModeHelper.removeAutomaticZenRule(systemId, modesApiFlag.mOriginForUserActionInSystemUi, + mZenModeHelper.removeAutomaticZenRule(systemId, modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); // In total, this represents 4 events @@ -3282,22 +3288,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_policyChanges(@TestParameter ModesApiFlag modesApiFlag) + public void testZenModeEventLog_policyChanges(@TestParameter ModesFlag modesFlag) throws IllegalArgumentException { - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); // First just turn zen mode on mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - modesApiFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, "", null, Process.SYSTEM_UID); // Now change the policy slightly; want to confirm that this'll be reflected in the logs ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); newConfig.allowAlarms = true; newConfig.allowRepeatCallers = false; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode // is off. @@ -3308,7 +3314,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { newConfig.allowMessages = false; newConfig.allowRepeatCallers = true; mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - modesApiFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, Process.SYSTEM_UID); // Total events: we only expect ones for turning on, changing policy, and turning off assertEquals(3, mZenModeEventLogger.numLoggedChanges()); @@ -3341,9 +3347,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_ruleCounts(@TestParameter ModesApiFlag modesApiFlag) + public void testZenModeEventLog_ruleCounts(@TestParameter ModesFlag modesFlag) throws IllegalArgumentException { - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3447,9 +3453,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenModeEventLog_noLogWithNoConfigChange( - @TestParameter ModesApiFlag modesApiFlag) throws IllegalArgumentException { + @TestParameter ModesFlag modesFlag) throws IllegalArgumentException { // If evaluateZenMode is called independently of a config change, don't log. - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3466,11 +3472,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testZenModeEventLog_reassignUid(@TestParameter ModesApiFlag modesApiFlag) + public void testZenModeEventLog_reassignUid(@TestParameter ModesFlag modesFlag) throws IllegalArgumentException { // Test that, only in specific cases, we reassign the calling UID to one associated with // the automatic rule owner. - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3496,7 +3502,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID); // Turn on rule 1; call looks like it's from the system. Because setting a condition is // typically an automatic (non-user-initiated) action, expect the calling UID to be @@ -3515,7 +3521,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // from the system-provided one. zenRule.setEnabled(false); mZenModeHelper.updateAutomaticZenRule(id, zenRule, - modesApiFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); + modesFlag.mOriginForUserActionInSystemUi, "", Process.SYSTEM_UID); // Add a manual rule. Any manual rule changes should not get calling uids reassigned. mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, UPDATE_ORIGIN_APP, @@ -3573,9 +3579,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenModeEventLog_channelsBypassingChanges( - @TestParameter ModesApiFlag modesApiFlag) { + @TestParameter ModesFlag modesFlag) { // Verify that the right thing happens when the canBypassDnd value changes. - modesApiFlag.applyFlag(mSetFlagsRule); + modesFlag.applyFlags(mSetFlagsRule); mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); @@ -3847,8 +3853,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() { + public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDefault( + @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) { + modesFlag.applyFlags(mSetFlagsRule); setupZenConfig(); // When there's one automatic rule active and it doesn't specify a policy, test that the @@ -3869,7 +3876,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // inspect the consolidated policy, which should match the device default settings. assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy)) - .isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + .isEqualTo(modesFlag == ModesFlag.MODES_UI + ? mZenModeHelper.getDefaultZenPolicy() + : mZenModeHelper.mConfig.toZenPolicy()); } @Test @@ -3904,7 +3913,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom - // policy for every field specified, and take default values for unspecified things + // policy for every field specified, and take default values (from device default or + // manual policy) for unspecified things assertTrue(mZenModeHelper.mConsolidatedPolicy.allowAlarms()); // custom assertTrue(mZenModeHelper.mConsolidatedPolicy.allowMedia()); // custom assertFalse(mZenModeHelper.mConsolidatedPolicy.allowSystem()); // default @@ -3918,8 +3928,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() { + public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDefault( + @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) { + modesFlag.applyFlags(mSetFlagsRule); setupZenConfig(); // when there's only one automatic rule active and it has a custom policy, make sure that's @@ -3948,11 +3959,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom - // policy for every field specified, and take default values for unspecified things - assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default - assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + // policy for every field specified, and take default values (from either device default + // policy or manual rule) for unspecified things + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? true : false); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? true : false); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // custom - assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? false : true); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse(); // custom @@ -4022,8 +4037,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() { + public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault( + @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) { + modesFlag.applyFlags(mSetFlagsRule); setupZenConfig(); // when there are two rules active, one inheriting the default policy and one setting its @@ -4071,16 +4087,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { // now both rules should be on, and the consolidated policy should reflect the most // restrictive option of each of the two assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // custom stricter - assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? true : false); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default stricter - assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? false : true); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom stricter assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()) .isFalse(); // custom stricter assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom stricter - assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse(); // default stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? false : true); // default } @Test @@ -4134,8 +4153,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll() { + public void testUpdateConsolidatedPolicy_ignoresActiveRulesWithInterruptionFilterAll( + @TestParameter({"MODES_UI", "MODES_API"}) ModesFlag modesFlag) { + modesFlag.applyFlags(mSetFlagsRule); setupZenConfig(); // Rules with INTERRUPTION_FILTER_ALL are skipped when calculating consolidated policy. @@ -4172,10 +4192,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); // Consolidated Policy should be default + rule1. - assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? true : false); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // priority rule - assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isEqualTo( + modesFlag == ModesFlag.MODES_UI ? false : true); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default @@ -6251,7 +6273,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) { - if (!Flags.modesApi()) { + if (!Flags.modesUi()) { checkDndProtoMatchesSetupZenConfig(dndProto); return; } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 10eae577f706..13550923cf3d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -46,7 +46,6 @@ import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; -import static android.server.wm.ActivityManagerTestBase.isTablet; import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -76,7 +75,6 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -124,7 +122,6 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.wm.BackgroundActivityStartController.BalVerdict; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; import com.android.server.wm.utils.MockTracker; -import com.android.window.flags.Flags; import org.junit.After; import org.junit.Before; @@ -1298,12 +1295,6 @@ public class ActivityStarterTests extends WindowTestsBase { */ @Test public void testDeliverIntentToTopActivityOfNonTopDisplay() { - // TODO(b/330152508): Remove check once legacy multi-display behaviour can coexist with - // desktop windowing mode - // Ignore test if desktop windowing is enabled on tablets as legacy multi-display - // behaviour will not be respected - assumeFalse(Flags.enableDesktopWindowingMode() && isTablet()); - final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, false /* mockGetRootTask */); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java index 695faa525dd6..39a2259cf77f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java @@ -19,6 +19,8 @@ package com.android.server.wm; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION; import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW; +import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; +import static com.android.window.flags.Flags.balImprovedMetrics; import static com.google.common.truth.Truth.assertThat; @@ -145,6 +147,16 @@ public class BackgroundActivityStartControllerTests { } @Override + boolean shouldLogStats(BalVerdict finalVerdict, BalState state) { + return true; + } + + @Override + boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) { + return true; + } + + @Override BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) { return mCallerVerdict.orElseGet( () -> super.checkBackgroundActivityStartAllowedByCaller(state)); @@ -238,7 +250,12 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict.getCode()).isEqualTo(BackgroundActivityStartController.BAL_BLOCK); - assertThat(mBalAllowedLogs).isEmpty(); // not allowed + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", BAL_BLOCK)); + } else { + assertThat(mBalAllowedLogs).isEmpty(); // not allowed + } } // Tests for BackgroundActivityStartController.checkBackgroundActivityStart @@ -268,7 +285,12 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict).isEqualTo(BalVerdict.BLOCK); - assertThat(mBalAllowedLogs).isEmpty(); // not allowed + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", BAL_BLOCK)); + } else { + assertThat(mBalAllowedLogs).isEmpty(); // not allowed + } } @Test @@ -298,7 +320,12 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict).isEqualTo(callerVerdict); - assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", callerVerdict.getCode())); + } else { + assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception + } } @Test @@ -362,7 +389,13 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict).isEqualTo(callerVerdict); - assertThat(mBalAllowedLogs).containsExactly(new BalAllowedLog("", callerVerdict.getCode())); + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", callerVerdict.getCode())); + } else { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("", callerVerdict.getCode())); + } } @Test @@ -398,7 +431,12 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict).isEqualTo(BalVerdict.BLOCK); - assertThat(mBalAllowedLogs).isEmpty(); + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", BAL_BLOCK)); + } else { + assertThat(mBalAllowedLogs).isEmpty(); + } } @Test @@ -430,7 +468,12 @@ public class BackgroundActivityStartControllerTests { // assertions assertThat(verdict).isEqualTo(callerVerdict); - assertThat(mBalAllowedLogs).isEmpty(); + if (balImprovedMetrics()) { + assertThat(mBalAllowedLogs).containsExactly( + new BalAllowedLog("package.app3/someClass", callerVerdict.getCode())); + } else { + assertThat(mBalAllowedLogs).isEmpty(); + } } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index b74da1a888cd..a60d243c9dad 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -85,6 +85,8 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.Property; import android.content.res.Resources; import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.InsetsSource; import android.view.InsetsState; @@ -96,6 +98,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.window.flags.Flags; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; @@ -1528,6 +1531,98 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mActivity.getParent().getConfiguration()), /* delta */ 0.01); } + @Test + public void testIsVerticalThinLetterboxed() { + // Vertical thin letterbox disabled + doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration) + .getThinLetterboxHeightPx(); + assertFalse(mController.isVerticalThinLetterboxed()); + // Define a Task 100x100 + final Task task = mock(Task.class); + doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); + doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration) + .getThinLetterboxHeightPx(); + + // Vertical thin letterbox disabled without Task + doReturn(null).when(mActivity).getTask(); + assertFalse(mController.isVerticalThinLetterboxed()); + // Assign a Task for the Activity + doReturn(task).when(mActivity).getTask(); + + // (task.width() - act.width()) / 2 = 5 < 10 + doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); + assertTrue(mController.isVerticalThinLetterboxed()); + + // (task.width() - act.width()) / 2 = 10 = 10 + doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); + assertTrue(mController.isVerticalThinLetterboxed()); + + // (task.width() - act.width()) / 2 = 11 > 10 + doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); + assertFalse(mController.isVerticalThinLetterboxed()); + } + + @Test + public void testIsHorizontalThinLetterboxed() { + // Horizontal thin letterbox disabled + doReturn(-1).when(mActivity.mWmService.mLetterboxConfiguration) + .getThinLetterboxWidthPx(); + assertFalse(mController.isHorizontalThinLetterboxed()); + // Define a Task 100x100 + final Task task = mock(Task.class); + doReturn(new Rect(0, 0, 100, 100)).when(task).getBounds(); + doReturn(10).when(mActivity.mWmService.mLetterboxConfiguration) + .getThinLetterboxWidthPx(); + + // Vertical thin letterbox disabled without Task + doReturn(null).when(mActivity).getTask(); + assertFalse(mController.isHorizontalThinLetterboxed()); + // Assign a Task for the Activity + doReturn(task).when(mActivity).getTask(); + + // (task.height() - act.height()) / 2 = 5 < 10 + doReturn(new Rect(5, 5, 95, 95)).when(mActivity).getBounds(); + assertTrue(mController.isHorizontalThinLetterboxed()); + + // (task.height() - act.height()) / 2 = 10 = 10 + doReturn(new Rect(10, 10, 90, 90)).when(mActivity).getBounds(); + assertTrue(mController.isHorizontalThinLetterboxed()); + + // (task.height() - act.height()) / 2 = 11 > 10 + doReturn(new Rect(11, 11, 89, 89)).when(mActivity).getBounds(); + assertFalse(mController.isHorizontalThinLetterboxed()); + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY) + public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { + spyOn(mController); + doReturn(true).when(mController).isVerticalThinLetterboxed(); + assertFalse(mController.allowVerticalReachabilityForThinLetterbox()); + doReturn(true).when(mController).isHorizontalThinLetterboxed(); + assertFalse(mController.allowHorizontalReachabilityForThinLetterbox()); + + doReturn(false).when(mController).isVerticalThinLetterboxed(); + assertTrue(mController.allowVerticalReachabilityForThinLetterbox()); + doReturn(false).when(mController).isHorizontalThinLetterboxed(); + assertTrue(mController.allowHorizontalReachabilityForThinLetterbox()); + } + + @Test + @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_REACHABILITY) + public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { + spyOn(mController); + doReturn(true).when(mController).isVerticalThinLetterboxed(); + assertTrue(mController.allowVerticalReachabilityForThinLetterbox()); + doReturn(true).when(mController).isHorizontalThinLetterboxed(); + assertTrue(mController.allowHorizontalReachabilityForThinLetterbox()); + + doReturn(false).when(mController).isVerticalThinLetterboxed(); + assertTrue(mController.allowVerticalReachabilityForThinLetterbox()); + doReturn(false).when(mController).isHorizontalThinLetterboxed(); + assertTrue(mController.allowHorizontalReachabilityForThinLetterbox()); + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 8677738f3edc..6b605ec6d0c0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -3646,11 +3646,27 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testIsReachabilityEnabled_thisLetterbox_false() { + // Case when the reachability would be enabled otherwise + setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); + mActivity.getWindowConfiguration().setBounds(null); + + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false); + + assertFalse(mActivity.mLetterboxUiController.isVerticalReachabilityEnabled()); + assertFalse(mActivity.mLetterboxUiController.isHorizontalReachabilityEnabled()); + } + + @Test public void testIsHorizontalReachabilityEnabled_splitScreen_false() { mAtm.mDevEnableNonResizableMultiWindow = true; setUpDisplaySizeWithApp(2800, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); @@ -3673,6 +3689,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(1000, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mActivity.getDisplayContent()); @@ -3694,6 +3711,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(1000, 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable landscape-only activity. prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_LANDSCAPE); @@ -3715,6 +3733,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(/* dw */ 1000, /* dh */ 2800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -3731,6 +3750,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(/* dw */ 2800, /* dh */ 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -3747,6 +3767,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait display setUpDisplaySizeWithApp(1400, 1600); mActivity.mWmService.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // 16:9f unresizable portrait app prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, @@ -3760,6 +3781,7 @@ public class SizeCompatTests extends WindowTestsBase { // Landscape display setUpDisplaySizeWithApp(1600, 1500); mActivity.mWmService.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // 16:9f unresizable landscape app prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, @@ -3773,6 +3795,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(2800, 1000); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable portrait-only activity. prepareUnresizable(mActivity, 1.1f, SCREEN_ORIENTATION_PORTRAIT); @@ -3794,6 +3817,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(1800, 2200); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable portrait-only activity. prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); @@ -3815,6 +3839,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpDisplaySizeWithApp(2200, 1800); mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true); + setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ true); // Unresizable landscape-only activity. prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); @@ -5011,6 +5036,14 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(activity.shouldSendCompatFakeFocus()); } + private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) { + spyOn(mActivity.mLetterboxUiController); + doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController) + .allowVerticalReachabilityForThinLetterbox(); + doReturn(thinLetterboxAllowed).when(mActivity.mLetterboxUiController) + .allowHorizontalReachabilityForThinLetterbox(); + } + private int getExpectedSplitSize(int dimensionToSplit) { int dividerWindowWidth = mActivity.mWmService.mContext.getResources().getDimensionPixelSize( diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 83e4151235ea..afa669807c2e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -487,6 +487,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Flush EVENT_APPEARED. mController.dispatchPendingEvents(); + // Even if the activity is not launched in an organized TaskFragment, it is still considered + // as the remote activity to the organizer process. Because when the task becomes visible, + // the organizer process needs to be interactive (unfrozen) to receive TaskFragment events. + activity.setVisibleRequested(true); + activity.setState(ActivityRecord.State.RESUMED, "test"); + assertTrue(organizerProc.hasVisibleActivities()); + activity.setVisibleRequested(false); + activity.setState(ActivityRecord.State.STOPPED, "test"); + assertFalse(organizerProc.hasVisibleActivities()); + // Make sure the activity belongs to the same app, but it is in a different pid. activity.info.applicationInfo.uid = uid; doReturn(pid + 1).when(activity).getPid(); diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 9d14290bdd8a..2e93cba80386 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -654,7 +654,10 @@ public class UsageStatsService extends SystemService implements } } else if (Intent.ACTION_USER_STARTED.equals(action)) { if (userId >= 0) { - mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget(); + if (!Flags.disableIdleCheck() || userId > 0) { + // Don't check idle state for USER_SYSTEM during the boot up. + mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget(); + } } } } @@ -2013,6 +2016,8 @@ public class UsageStatsService extends SystemService implements + ": " + Flags.useParceledList()); pw.println(" " + Flags.FLAG_FILTER_BASED_EVENT_QUERY_API + ": " + Flags.filterBasedEventQueryApi()); + pw.println(" " + Flags.FLAG_DISABLE_IDLE_CHECK + + ": " + Flags.disableIdleCheck()); final int[] userIds; synchronized (mLock) { diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java index 40537c85784d..6d53c274cdf4 100644 --- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java +++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java @@ -16,6 +16,8 @@ package com.android.server.usb; +import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; + import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE; import android.Manifest; @@ -43,6 +45,7 @@ import android.os.AsyncTask; import android.os.Environment; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.usb.UsbProfileGroupSettingsManagerProto; import android.service.usb.UsbSettingsAccessoryPreferenceProto; import android.service.usb.UsbSettingsDevicePreferenceProto; @@ -939,13 +942,24 @@ public class UsbProfileGroupSettingsManager { } /** - * @return true if any application in foreground have set restrict_usb_overlay_activities as - * true in manifest file. The application needs to have MANAGE_USB permission. + * @return true if the user has not finished the setup process or if there are any + * foreground applications with MANAGE_USB permission and restrict_usb_overlay_activities + * enabled in the manifest file. */ private boolean shouldRestrictOverlayActivities() { if (!Flags.allowRestrictionOfOverlayActivities()) return false; + if (Settings.Secure.getIntForUser( + mContext.getContentResolver(), + USER_SETUP_COMPLETE, + /* defaultValue= */ 1, + UserHandle.CURRENT.getIdentifier()) + == 0) { + Slog.d(TAG, "restricting usb overlay activities as setup is not complete"); + return true; + } + List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager .getRunningAppProcesses(); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 0fc9d6ff1501..3b9ee807077e 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -28,14 +28,11 @@ import android.os.InputEventInjectionSync import android.os.SystemClock import android.os.test.TestLooper import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.provider.Settings import android.view.View.OnKeyListener -import android.view.Display import android.view.InputDevice import android.view.KeyEvent -import android.view.PointerIcon import android.view.SurfaceHolder import android.view.SurfaceView import android.test.mock.MockContentResolver @@ -44,7 +41,6 @@ import com.android.internal.util.test.FakeSettingsProvider import com.google.common.truth.Truth.assertThat import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity import org.junit.After -import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -53,22 +49,16 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.`when` -import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.verifyZeroInteractions import org.mockito.junit.MockitoJUnit import org.mockito.stubbing.OngoingStubbing -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * Tests for {@link InputManagerService}. @@ -187,197 +177,6 @@ class InputManagerServiceTests { verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y) } - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun testSetVirtualMousePointerDisplayId() { - // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked - // until the native callback happens. - var countDownLatch = CountDownLatch(1) - val overrideDisplayId = 123 - Thread { - assertTrue("Setting virtual pointer display should succeed", - localService.setVirtualMousePointerDisplayId(overrideDisplayId)) - countDownLatch.countDown() - }.start() - assertFalse("Setting virtual pointer display should block", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - - val x = 42f - val y = 314f - service.onPointerDisplayIdChanged(overrideDisplayId, x, y) - testLooper.dispatchNext() - verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y) - assertTrue("Native callback unblocks calling thread", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - verify(native).setPointerDisplayId(overrideDisplayId) - - // Ensure that setting the same override again succeeds immediately. - assertTrue("Setting the same virtual mouse pointer displayId again should succeed", - localService.setVirtualMousePointerDisplayId(overrideDisplayId)) - - // Ensure that we did not query WM for the pointerDisplayId when setting the override - verify(wmCallbacks, never()).pointerDisplayId - - // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new - // pointer displayId and the calling thread is blocked until the native callback happens. - countDownLatch = CountDownLatch(1) - val pointerDisplayId = 42 - `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId) - Thread { - assertTrue("Unsetting virtual mouse pointer displayId should succeed", - localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY)) - countDownLatch.countDown() - }.start() - assertFalse("Unsetting virtual mouse pointer displayId should block", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - - service.onPointerDisplayIdChanged(pointerDisplayId, x, y) - testLooper.dispatchNext() - verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y) - assertTrue("Native callback unblocks calling thread", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - verify(native).setPointerDisplayId(pointerDisplayId) - } - - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() { - // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked - // until the native callback happens. - val countDownLatch = CountDownLatch(1) - val overrideDisplayId = 123 - Thread { - assertFalse("Setting virtual pointer display should be unsuccessful", - localService.setVirtualMousePointerDisplayId(overrideDisplayId)) - countDownLatch.countDown() - }.start() - assertFalse("Setting virtual pointer display should block", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - - val x = 42f - val y = 314f - // Assume the native callback updates the pointerDisplayId to the incorrect value. - service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y) - testLooper.dispatchNext() - verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y) - assertTrue("Native callback unblocks calling thread", - countDownLatch.await(100, TimeUnit.MILLISECONDS)) - verify(native).setPointerDisplayId(overrideDisplayId) - } - - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun testSetVirtualMousePointerDisplayId_competingRequests() { - val firstRequestSyncLatch = CountDownLatch(1) - doAnswer { - firstRequestSyncLatch.countDown() - }.`when`(native).setPointerDisplayId(anyInt()) - - val firstRequestLatch = CountDownLatch(1) - val firstOverride = 123 - Thread { - assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful", - localService.setVirtualMousePointerDisplayId(firstOverride)) - firstRequestLatch.countDown() - }.start() - assertFalse("Setting virtual pointer display should block", - firstRequestLatch.await(100, TimeUnit.MILLISECONDS)) - - assertTrue("Wait for first thread's request should succeed", - firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS)) - - val secondRequestLatch = CountDownLatch(1) - val secondOverride = 42 - Thread { - assertTrue("Setting virtual mouse pointer from thread 2 should be successful", - localService.setVirtualMousePointerDisplayId(secondOverride)) - secondRequestLatch.countDown() - }.start() - assertFalse("Setting virtual mouse pointer should block", - secondRequestLatch.await(100, TimeUnit.MILLISECONDS)) - - val x = 42f - val y = 314f - // Assume the native callback updates directly to the second request. - service.onPointerDisplayIdChanged(secondOverride, x, y) - testLooper.dispatchNext() - verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y) - assertTrue("Native callback unblocks first thread", - firstRequestLatch.await(100, TimeUnit.MILLISECONDS)) - assertTrue("Native callback unblocks second thread", - secondRequestLatch.await(100, TimeUnit.MILLISECONDS)) - verify(native, times(2)).setPointerDisplayId(anyInt()) - } - - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun onDisplayRemoved_resetAllAdditionalInputProperties() { - setVirtualMousePointerDisplayIdAndVerify(10) - - localService.setPointerIconVisible(false, 10) - verify(native).setPointerIconVisibility(10, false) - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(10, false) - - service.onDisplayRemoved(10) - verify(native).setPointerIconVisibility(10, true) - verify(native).displayRemoved(eq(10)) - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) - verify(native).setMousePointerAccelerationEnabled(10, true) - verifyNoMoreInteractions(native) - - // This call should not block because the virtual mouse pointer override was never removed. - localService.setVirtualMousePointerDisplayId(10) - - verify(native).setPointerDisplayId(eq(10)) - verifyNoMoreInteractions(native) - } - - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun updateAdditionalInputPropertiesForOverrideDisplay() { - setVirtualMousePointerDisplayIdAndVerify(10) - - localService.setPointerIconVisible(false, 10) - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - verify(native).setPointerIconVisibility(10, false) - localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(10, false) - - localService.setPointerIconVisible(true, 10) - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) - verify(native).setPointerIconVisibility(10, true) - localService.setMousePointerAccelerationEnabled(true, 10) - verify(native).setMousePointerAccelerationEnabled(10, true) - - localService.setPointerIconVisible(false, 20) - verify(native).setPointerIconVisibility(20, false) - localService.setMousePointerAccelerationEnabled(false, 20) - verify(native).setMousePointerAccelerationEnabled(20, false) - verifyNoMoreInteractions(native) - - clearInvocations(native) - setVirtualMousePointerDisplayIdAndVerify(20) - - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - } - - @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) - @Test - fun setAdditionalInputPropertiesBeforeOverride() { - localService.setPointerIconVisible(false, 10) - localService.setMousePointerAccelerationEnabled(false, 10) - - verify(native).setPointerIconVisibility(10, false) - verify(native).setMousePointerAccelerationEnabled(10, false) - verifyNoMoreInteractions(native) - - setVirtualMousePointerDisplayIdAndVerify(10) - - verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - } - @Test fun setDeviceTypeAssociation_setsDeviceTypeAssociation() { val inputPort = "inputPort" @@ -412,20 +211,6 @@ class InputManagerServiceTests { verify(native, times(2)).changeKeyboardLayoutAssociation() } - private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) { - val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) } - thread.start() - - // Allow some time for the set override call to park while waiting for the native callback. - Thread.sleep(100 /*millis*/) - verify(native).setPointerDisplayId(overrideDisplayId) - - service.onPointerDisplayIdChanged(overrideDisplayId, 0f, 0f) - testLooper.dispatchNext() - verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f) - thread.join(100 /*millis*/) - } - private fun createVirtualDisplays(count: Int): List<VirtualDisplay> { val displayManager: DisplayManager = context.getSystemService( DisplayManager::class.java diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp index f0bea3f3c28a..2909e66b53be 100644 --- a/tests/UsbManagerTests/Android.bp +++ b/tests/UsbManagerTests/Android.bp @@ -43,6 +43,9 @@ android_test { "libmultiplejvmtiagentsinterferenceagent", "libstaticjvmtiagent", ], + libs: [ + "android.test.mock", + ], certificate: "platform", platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java index 4780d8a610e8..87b26a63acc7 100644 --- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java +++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java @@ -16,6 +16,8 @@ package com.android.server.usbtest; +import static android.provider.Settings.Secure.USER_SETUP_COMPLETE; + import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES; import static org.mockito.ArgumentMatchers.any; @@ -32,16 +34,20 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.Property; import android.content.pm.UserInfo; import android.content.res.Resources; import android.hardware.usb.UsbDevice; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; +import android.test.mock.MockContentResolver; import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.usb.UsbHandlerManager; import com.android.server.usb.UsbProfileGroupSettingsManager; import com.android.server.usb.UsbSettingsManager; @@ -69,6 +75,7 @@ import java.util.List; public class UsbProfileGroupSettingsManagerTest { private static final String TEST_PACKAGE_NAME = "testPkg"; + @Mock private Context mContext; @Mock @@ -85,43 +92,78 @@ public class UsbProfileGroupSettingsManagerTest { private UserManager mUserManager; @Mock private UsbUserSettingsManager mUsbUserSettingsManager; - @Mock private Property mProperty; - private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo; - private PackageInfo mPackageInfo; - private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager; + @Mock + private Property mRestrictUsbOverlayActivitiesProperty; + @Mock + private UsbDevice mUsbDevice; + + private MockContentResolver mContentResolver; private MockitoSession mStaticMockSession; + private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mStaticMockSession = ExtendedMockito.mockitoSession() + .mockStatic(Flags.class) + .strictness(Strictness.WARN) + .startMocking(); - mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo(); - mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME}; - mPackageInfo = new PackageInfo(); - mPackageInfo.packageName = TEST_PACKAGE_NAME; - mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); + when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager); + when(mUserManager.getEnabledProfiles(anyInt())) + .thenReturn(List.of(Mockito.mock(UserInfo.class))); + + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); - when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager); when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class)); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager); when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class))) .thenReturn(mContext); - when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); - mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle, - mUsbSettingsManager, mUsbHandlerManager); + mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager( + mContext, mUserHandle, mUsbSettingsManager, mUsbHandlerManager); - mStaticMockSession = ExtendedMockito.mockitoSession() - .mockStatic(Flags.class) - .strictness(Strictness.WARN) - .startMocking(); + setupDefaultConfiguration(); + } + /** + * Setups the following configuration + * + * <ul> + * <li>Flag is enabled + * <li>Device setup has completed + * <li>There is a foreground activity with MANAGE_USB permission + * <li>The foreground activity has PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES enabled + * </ul> + */ + private void setupDefaultConfiguration() throws NameNotFoundException { + when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + + Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 1); + + ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo = + new ActivityManager.RunningAppProcessInfo(); + mRunningAppProcessInfo.pkgList = new String[] { TEST_PACKAGE_NAME }; + when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); + + PackageInfo mPackageInfo = new PackageInfo(); + mPackageInfo.packageName = TEST_PACKAGE_NAME; + mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo); - when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES), - eq(TEST_PACKAGE_NAME))).thenReturn(mProperty); - when(mUserManager.getEnabledProfiles(anyInt())) - .thenReturn(List.of(Mockito.mock(UserInfo.class))); - when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager); + when(mPackageManager.getPackagesHoldingPermissions( + new String[] { android.Manifest.permission.MANAGE_USB }, + PackageManager.MATCH_SYSTEM_ONLY)) + .thenReturn(List.of(mPackageInfo)); + + when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(true); + when(mPackageManager.getProperty( + eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES), eq(TEST_PACKAGE_NAME))) + .thenReturn(mRestrictUsbOverlayActivitiesProperty); } @After @@ -130,66 +172,59 @@ public class UsbProfileGroupSettingsManagerTest { } @Test - public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() { - when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); + public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() { + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); + + verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class)); + } + + @Test + public void testDeviceAttached_noForegroundActivity_resolveActivityCalled() { when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>()); - when(mPackageManager.getPackagesHoldingPermissions( - new String[]{android.Manifest.permission.MANAGE_USB}, - PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); - UsbDevice device = Mockito.mock(UsbDevice.class); - mUsbProfileGroupSettingsManager.deviceAttached(device); + + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); } @Test public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() { - when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); - when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); when(mPackageManager.getPackagesHoldingPermissions( - new String[]{android.Manifest.permission.MANAGE_USB}, - PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>()); - UsbDevice device = Mockito.mock(UsbDevice.class); - mUsbProfileGroupSettingsManager.deviceAttached(device); + new String[] { android.Manifest.permission.MANAGE_USB }, + PackageManager.MATCH_SYSTEM_ONLY)) + .thenReturn(new ArrayList<>()); + + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); } @Test - public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() { - when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); - when(mProperty.getBoolean()).thenReturn(true); - when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); - when(mPackageManager.getPackagesHoldingPermissions( - new String[]{android.Manifest.permission.MANAGE_USB}, - PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); - UsbDevice device = Mockito.mock(UsbDevice.class); - mUsbProfileGroupSettingsManager.deviceAttached(device); - verify(mUsbUserSettingsManager, times(0)) - .queryIntentActivities(any(Intent.class)); - } + public void testDeviceAttached_restricUsbOverlayPropertyDisabled_resolveActivityCalled() { + when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false); + + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); - @Test - public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() { - when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true); - when(mProperty.getBoolean()).thenReturn(false); - when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); - when(mPackageManager.getPackagesHoldingPermissions( - new String[]{android.Manifest.permission.MANAGE_USB}, - PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); - UsbDevice device = Mockito.mock(UsbDevice.class); - mUsbProfileGroupSettingsManager.deviceAttached(device); verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); } @Test - public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() { + public void testDeviceAttached_flagFalse_resolveActivityCalled() { when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false); - when(mProperty.getBoolean()).thenReturn(true); - when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo)); - when(mPackageManager.getPackagesHoldingPermissions( - new String[]{android.Manifest.permission.MANAGE_USB}, - PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo)); - UsbDevice device = Mockito.mock(UsbDevice.class); - mUsbProfileGroupSettingsManager.deviceAttached(device); + + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); + verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class)); } + + @Test + public void + testDeviceAttached_setupNotCompleteAndNoBlockingActivities_resolveActivityNotCalled() { + when(mRestrictUsbOverlayActivitiesProperty.getBoolean()).thenReturn(false); + Settings.Secure.putInt(mContentResolver, USER_SETUP_COMPLETE, 0); + + mUsbProfileGroupSettingsManager.deviceAttached(mUsbDevice); + + verify(mUsbUserSettingsManager, times(0)).queryIntentActivities(any(Intent.class)); + } } diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 960b57cb632a..580efe126ea3 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -70,6 +70,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.Uri; +import android.net.vcn.Flags; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; import android.net.vcn.VcnConfig; @@ -82,7 +83,9 @@ import android.os.ParcelUuid; import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; +import android.os.UserManager; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -101,6 +104,7 @@ import com.android.server.vcn.util.PersistableBundleUtils; import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -118,6 +122,8 @@ import java.util.UUID; @RunWith(AndroidJUnit4.class) @SmallTest public class VcnManagementServiceTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String CONTEXT_ATTRIBUTION_TAG = "VCN"; private static final String TEST_PACKAGE_NAME = VcnManagementServiceTest.class.getPackage().getName(); @@ -129,7 +135,12 @@ public class VcnManagementServiceTest { private static final ParcelUuid TEST_UUID_3 = new ParcelUuid(new UUID(2, 2)); private static final VcnConfig TEST_VCN_CONFIG; private static final VcnConfig TEST_VCN_CONFIG_PKG_2; - private static final int TEST_UID = Process.FIRST_APPLICATION_UID; + + private static final int TEST_UID = 1010000; // A non-system user + private static final UserHandle TEST_USER_HANDLE = UserHandle.getUserHandleForUid(TEST_UID); + private static final UserHandle TEST_USER_HANDLE_OTHER = + UserHandle.of(TEST_USER_HANDLE.getIdentifier() + 1); + private static final String TEST_IFACE_NAME = "TEST_IFACE"; private static final String TEST_IFACE_NAME_2 = "TEST_IFACE2"; private static final LinkProperties TEST_LP_1 = new LinkProperties(); @@ -187,6 +198,7 @@ public class VcnManagementServiceTest { private final TelephonyManager mTelMgr = mock(TelephonyManager.class); private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class); private final AppOpsManager mAppOpsMgr = mock(AppOpsManager.class); + private final UserManager mUserManager = mock(UserManager.class); private final VcnContext mVcnContext = mock(VcnContext.class); private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper = mock(PersistableBundleUtils.LockingReadWriteHelper.class); @@ -218,6 +230,9 @@ public class VcnManagementServiceTest { Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class); setupSystemService(mMockContext, mAppOpsMgr, Context.APP_OPS_SERVICE, AppOpsManager.class); + setupSystemService(mMockContext, mUserManager, Context.USER_SERVICE, UserManager.class); + + doReturn(TEST_USER_HANDLE).when(mUserManager).getMainUser(); doReturn(TEST_PACKAGE_NAME).when(mMockContext).getOpPackageName(); @@ -267,6 +282,8 @@ public class VcnManagementServiceTest { @Before public void setUp() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENFORCE_MAIN_USER); + doNothing() .when(mMockContext) .enforceCallingOrSelfPermission( @@ -717,10 +734,8 @@ public class VcnManagementServiceTest { } @Test - public void testSetVcnConfigRequiresSystemUser() throws Exception { - doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID)) - .when(mMockDeps) - .getBinderCallingUid(); + public void testSetVcnConfigRequiresMainUser() throws Exception { + doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser(); try { mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME); @@ -832,10 +847,8 @@ public class VcnManagementServiceTest { } @Test - public void testClearVcnConfigRequiresSystemUser() throws Exception { - doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID)) - .when(mMockDeps) - .getBinderCallingUid(); + public void testClearVcnConfigRequiresMainUser() throws Exception { + doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser(); try { mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1, TEST_PACKAGE_NAME); @@ -921,10 +934,8 @@ public class VcnManagementServiceTest { } @Test - public void testGetConfiguredSubscriptionGroupsRequiresSystemUser() throws Exception { - doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID)) - .when(mMockDeps) - .getBinderCallingUid(); + public void testGetConfiguredSubscriptionGroupsRequiresMainUser() throws Exception { + doReturn(TEST_USER_HANDLE_OTHER).when(mUserManager).getMainUser(); try { mVcnMgmtSvc.getConfiguredSubscriptionGroups(TEST_PACKAGE_NAME); diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING index f6885e1e74ba..856e6eefba15 100644 --- a/tools/hoststubgen/TEST_MAPPING +++ b/tools/hoststubgen/TEST_MAPPING @@ -1,63 +1,7 @@ -// Keep the following two TEST_MAPPINGs in sync: -// frameworks/base/ravenwood/TEST_MAPPING -// frameworks/base/tools/hoststubgen/TEST_MAPPING { - "presubmit": [ - { "name": "tiny-framework-dump-test" }, - { "name": "hoststubgentest" }, - { "name": "hoststubgen-invoke-test" }, + "imports": [ { - "name": "RavenwoodMockitoTest_device" - }, - { - "name": "RavenwoodBivalentTest_device" - }, - // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING - { - "name": "SystemUIGoogleTests", - "options": [ - { - "exclude-annotation": "org.junit.Ignore" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - } - ] - } - ], - "presubmit-large": [ - { - "name": "SystemUITests", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation": "org.junit.Ignore" - } - ] - } - ], - "ravenwood-presubmit": [ - { - "name": "RavenwoodMinimumTest", - "host": true - }, - { - "name": "RavenwoodMockitoTest", - "host": true - }, - { - "name": "CtsUtilTestCasesRavenwood", - "host": true - }, - { - "name": "RavenwoodCoreTest", - "host": true - }, - { - "name": "RavenwoodBivalentTest", - "host": true + "path": "frameworks/base/ravenwood" } ] } diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig index 3c734bc7e1e3..c5bc0396de34 100644 --- a/wifi/wifi.aconfig +++ b/wifi/wifi.aconfig @@ -9,3 +9,11 @@ flag { bug: "313038031" is_fixed_read_only: true } + +flag { + name: "network_provider_battery_charging_status" + is_exported: true + namespace: "wifi" + description: "Control the API that allows setting / reading the NetworkProviderInfo's battery charging status" + bug: "305067231" +} |