diff --git a/AconfigFlags.bp b/AconfigFlags.bp index c211b02029253165b95e25f47787e40ea735a5a7..76377e72fb6ba7e354dbb5ff34c3d05dc4799795 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -29,6 +29,7 @@ aconfig_srcjars = [ ":android.service.notification.flags-aconfig-java{.generated_srcjars}", ":android.view.flags-aconfig-java{.generated_srcjars}", ":android.view.accessibility.flags-aconfig-java{.generated_srcjars}", + ":audio-framework-aconfig", ":camera_platform_flags_core_java_lib{.generated_srcjars}", ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", @@ -39,7 +40,6 @@ aconfig_srcjars = [ ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", ":android.widget.flags-aconfig-java{.generated_srcjars}", - ":com.android.media.audio.flags-aconfig-java{.generated_srcjars}", ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", ":sdk_sandbox_flags_lib{.generated_srcjars}", ":android.permission.flags-aconfig-java{.generated_srcjars}", @@ -54,7 +54,6 @@ aconfig_srcjars = [ ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}", ":android.service.voice.flags-aconfig-java{.generated_srcjars}", ":android.media.tv.flags-aconfig-java{.generated_srcjars}", - ":aconfig_midi_flags_java_lib{.generated_srcjars}", ":android.service.autofill.flags-aconfig-java{.generated_srcjars}", ":com.android.net.flags-aconfig-java{.generated_srcjars}", ":device_policy_aconfig_flags_lib{.generated_srcjars}", @@ -393,13 +392,6 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } -// Media Audio -java_aconfig_library { - name: "com.android.media.audio.flags-aconfig-java", - aconfig_declarations: "aconfig_audio_flags", - defaults: ["framework-minus-apex-aconfig-java-defaults"], -} - // Permissions aconfig_declarations { name: "android.permission.flags-aconfig", diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 646e05c50a762773c360ed6b80d4fac6d37f9fb1..9961c4fdf3f76da15429f7ac7cf18c41cb5e8a07 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -1791,7 +1791,10 @@ public class JobInfo implements Parcelable { * <ol> * <li>Run as soon as possible</li> * <li>Be less restricted during Doze and battery saver</li> - * <li>Bypass Doze, app standby, and battery saver network restrictions</li> + * <li> + * Bypass Doze, app standby, and battery saver network restrictions (if the job + * has a {@link #setRequiredNetwork(NetworkRequest) connectivity constraint}) + * </li> * <li>Be less likely to be killed than regular jobs</li> * <li>Be subject to background location throttling</li> * <li>Be exempt from delay to optimize job execution</li> 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 23b36e20b17499c2b3684403775ec08ecffc70a4..bff43534ce059355ee47e04b90d618e4be818a59 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -3859,10 +3859,16 @@ public class JobSchedulerService extends com.android.server.SystemService // Only let the app use the higher runtime if it hasn't repeatedly timed out. final String timeoutTag = job.shouldTreatAsExpeditedJob() ? QUOTA_TRACKER_TIMEOUT_EJ_TAG : QUOTA_TRACKER_TIMEOUT_REG_TAG; + // Developers are informed that expedited jobs can be stopped earlier than regular jobs + // and so shouldn't use them for long pieces of work. There's little reason to let + // them run longer than the normal 10 minutes. + final long normalUpperLimitMs = job.shouldTreatAsExpeditedJob() + ? mConstants.RUNTIME_MIN_GUARANTEE_MS + : mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS; final long upperLimitMs = mQuotaTracker.isWithinQuota(job.getTimeoutBlameUserId(), job.getTimeoutBlamePackageName(), timeoutTag) - ? mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS + ? normalUpperLimitMs : mConstants.RUNTIME_MIN_GUARANTEE_MS; return Math.min(upperLimitMs, mConstants.USE_TARE_POLICY diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 2d49cfb3b1712c19fe0427e0419569da77dc8839..721a8bdce57a5bc8366105ec334666b2f23d3747 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -427,34 +427,30 @@ public final class JobServiceContext implements ServiceConnection { boolean binding = false; boolean startedWithForegroundFlag = false; try { - final Context.BindServiceFlags bindFlags; + long bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_APP_COMPONENT_USAGE; if (job.shouldTreatAsUserInitiatedJob() && !job.isUserBgRestricted()) { // If the user has bg restricted the app, don't give the job FG privileges // such as bypassing data saver or getting the higher foreground proc state. // If we've gotten to this point, the app is most likely in the foreground, // so the job will run just fine while the user keeps the app in the foreground. - bindFlags = Context.BindServiceFlags.of( - Context.BIND_AUTO_CREATE - | Context.BIND_ALMOST_PERCEPTIBLE - | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS - | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS - | Context.BIND_NOT_APP_COMPONENT_USAGE); + bindFlags |= Context.BIND_ALMOST_PERCEPTIBLE; + if (job.hasConnectivityConstraint()) { + // Only add network restriction bypass flags if the job requires network. + bindFlags |= Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS + | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS; + } startedWithForegroundFlag = true; } else if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) { - bindFlags = Context.BindServiceFlags.of( - Context.BIND_AUTO_CREATE - | Context.BIND_NOT_FOREGROUND - | Context.BIND_ALMOST_PERCEPTIBLE - | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS - | Context.BIND_NOT_APP_COMPONENT_USAGE); + bindFlags |= Context.BIND_NOT_FOREGROUND | Context.BIND_ALMOST_PERCEPTIBLE; + if (job.hasConnectivityConstraint()) { + // Only add network restriction bypass flags if the job requires network. + bindFlags |= Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS; + } } else { - bindFlags = Context.BindServiceFlags.of( - Context.BIND_AUTO_CREATE - | Context.BIND_NOT_FOREGROUND - | Context.BIND_NOT_PERCEPTIBLE - | Context.BIND_NOT_APP_COMPONENT_USAGE); + bindFlags |= Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_PERCEPTIBLE; } - binding = mContext.bindServiceAsUser(intent, this, bindFlags, + binding = mContext.bindServiceAsUser(intent, this, + Context.BindServiceFlags.of(bindFlags), UserHandle.of(job.getUserId())); } catch (SecurityException e) { // Some permission policy, for example INTERACT_ACROSS_USERS and diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java index a25af7110ee55fc107777e4ba3f3e599d9feadbc..47d3fd5bc8c43ab8becf61d831b8de31d9dfdcd8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java @@ -18,13 +18,16 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import com.android.server.job.controllers.idle.CarIdlenessTracker; @@ -89,6 +92,19 @@ public final class IdleController extends RestrictingController implements Idlen } } + @Override + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + mIdleTracker.processConstant(properties, key); + } + + @Override + @GuardedBy("mLock") + public void onBatteryStateChangedLocked() { + mIdleTracker.onBatteryStateChanged( + mService.isBatteryCharging(), mService.isBatteryNotLow()); + } + /** * State-change notifications from the idleness tracker */ @@ -119,7 +135,16 @@ public final class IdleController extends RestrictingController implements Idlen } else { mIdleTracker = new DeviceIdlenessTracker(); } - mIdleTracker.startTracking(ctx, this); + mIdleTracker.startTracking(ctx, mService, this); + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.println(); + pw.println("IdleController:"); + pw.increaseIndent(); + mIdleTracker.dumpConstants(pw); + pw.decreaseIndent(); } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java index c458caec28735ff8f12500fdbab52feb1f74512d..ba0e633975053d537ab6b12573512c6e06ec73c6 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java @@ -16,10 +16,13 @@ package com.android.server.job.controllers.idle; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; @@ -73,7 +76,8 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen } @Override - public void startTracking(Context context, IdlenessListener listener) { + public void startTracking(Context context, JobSchedulerService service, + IdlenessListener listener) { mIdleListener = listener; IntentFilter filter = new IntentFilter(); @@ -94,6 +98,15 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen context.registerReceiver(this, filter, null, AppSchedulingModuleThread.getHandler()); } + /** Process the specified constant and update internal constants if relevant. */ + public void processConstant(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + } + + @Override + public void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow) { + } + @Override public void dump(PrintWriter pw) { pw.print(" mIdle: "); pw.println(mIdle); @@ -118,6 +131,10 @@ public final class CarIdlenessTracker extends BroadcastReceiver implements Idlen proto.end(token); } + @Override + public void dumpConstants(IndentingPrintWriter pw) { + } + @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java index c943e73eb12cf2b67c2f30c86dfa4fc5d73e8994..7dd3d13433797fa0cf4f7f478c48bd470f3d73b1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java @@ -17,9 +17,12 @@ package com.android.server.job.controllers.idle; import static android.app.UiModeManager.PROJECTION_TYPE_NONE; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; +import android.annotation.NonNull; import android.app.AlarmManager; import android.app.UiModeManager; import android.content.BroadcastReceiver; @@ -27,10 +30,13 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.PowerManager; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; import com.android.server.am.ActivityManagerService; import com.android.server.job.JobSchedulerService; @@ -45,17 +51,38 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private static final boolean DEBUG = JobSchedulerService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); + /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */ + private static final String IC_DIT_CONSTANT_PREFIX = "ic_dit_"; + @VisibleForTesting + static final String KEY_INACTIVITY_IDLE_THRESHOLD_MS = + IC_DIT_CONSTANT_PREFIX + "inactivity_idle_threshold_ms"; + @VisibleForTesting + static final String KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS = + IC_DIT_CONSTANT_PREFIX + "inactivity_idle_stable_power_threshold_ms"; + private static final String KEY_IDLE_WINDOW_SLOP_MS = + IC_DIT_CONSTANT_PREFIX + "idle_window_slop_ms"; + private AlarmManager mAlarm; private PowerManager mPowerManager; // After construction, mutations of idle/screen-on/projection states will only happen // on the JobScheduler thread, either in onReceive(), in an alarm callback, or in on.*Changed. private long mInactivityIdleThreshold; + private long mInactivityStablePowerIdleThreshold; private long mIdleWindowSlop; + /** Stable power is defined as "charging + battery not low." */ + private boolean mIsStablePower; private boolean mIdle; private boolean mScreenOn; private boolean mDockIdle; private boolean mProjectionActive; + + /** + * Time (in the elapsed realtime timebase) when the idleness check was scheduled. This should + * be a negative value if the device is not in state to be considered idle. + */ + private long mIdlenessCheckScheduledElapsed = -1; + private IdlenessListener mIdleListener; private final UiModeManager.OnProjectionStateChangedListener mOnProjectionStateChangedListener = this::onProjectionStateChanged; @@ -76,10 +103,14 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id } @Override - public void startTracking(Context context, IdlenessListener listener) { + public void startTracking(Context context, JobSchedulerService service, + IdlenessListener listener) { mIdleListener = listener; mInactivityIdleThreshold = context.getResources().getInteger( com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + mInactivityStablePowerIdleThreshold = context.getResources().getInteger( + com.android.internal.R.integer + .config_jobSchedulerInactivityIdleThresholdOnStablePower); mIdleWindowSlop = context.getResources().getInteger( com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop); mAlarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); @@ -107,6 +138,46 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id context.getSystemService(UiModeManager.class).addOnProjectionStateChangedListener( UiModeManager.PROJECTION_TYPE_ALL, AppSchedulingModuleThread.getExecutor(), mOnProjectionStateChangedListener); + + mIsStablePower = service.isBatteryCharging() && service.isBatteryNotLow(); + } + + /** Process the specified constant and update internal constants if relevant. */ + public void processConstant(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + switch (key) { + case KEY_INACTIVITY_IDLE_THRESHOLD_MS: + // Keep the threshold in the range [1 minute, 4 hours]. + mInactivityIdleThreshold = Math.max(MINUTE_IN_MILLIS, Math.min(4 * HOUR_IN_MILLIS, + properties.getLong(key, mInactivityIdleThreshold))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + case KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS: + // Keep the threshold in the range [1 minute, 4 hours]. + mInactivityStablePowerIdleThreshold = Math.max(MINUTE_IN_MILLIS, + Math.min(4 * HOUR_IN_MILLIS, + properties.getLong(key, mInactivityStablePowerIdleThreshold))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + case KEY_IDLE_WINDOW_SLOP_MS: + // Keep the slop in the range [1 minute, 15 minutes]. + mIdleWindowSlop = Math.max(MINUTE_IN_MILLIS, Math.min(15 * MINUTE_IN_MILLIS, + properties.getLong(key, mIdleWindowSlop))); + // Don't bother updating any pending alarms. Just wait until the next time we + // attempt to check for idle state to use the new value. + break; + } + } + + @Override + public void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow) { + final boolean isStablePower = isCharging && isBatteryNotLow; + if (mIsStablePower != isStablePower) { + mIsStablePower = isStablePower; + maybeScheduleIdlenessCheck("stable power changed"); + } } private void onProjectionStateChanged(@UiModeManager.ProjectionType int activeProjectionTypes, @@ -134,8 +205,10 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id public void dump(PrintWriter pw) { pw.print(" mIdle: "); pw.println(mIdle); pw.print(" mScreenOn: "); pw.println(mScreenOn); + pw.print(" mIsStablePower: "); pw.println(mIsStablePower); pw.print(" mDockIdle: "); pw.println(mDockIdle); pw.print(" mProjectionActive: "); pw.println(mProjectionActive); + pw.print(" mIdlenessCheckScheduledElapsed: "); pw.println(mIdlenessCheckScheduledElapsed); } @Override @@ -161,6 +234,17 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id proto.end(token); } + @Override + public void dumpConstants(IndentingPrintWriter pw) { + pw.println("DeviceIdlenessTracker:"); + pw.increaseIndent(); + pw.print(KEY_INACTIVITY_IDLE_THRESHOLD_MS, mInactivityIdleThreshold).println(); + pw.print(KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS, mInactivityStablePowerIdleThreshold) + .println(); + pw.print(KEY_IDLE_WINDOW_SLOP_MS, mIdleWindowSlop).println(); + pw.decreaseIndent(); + } + @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); @@ -220,9 +304,24 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private void maybeScheduleIdlenessCheck(String reason) { if ((!mScreenOn || mDockIdle) && !mProjectionActive) { final long nowElapsed = sElapsedRealtimeClock.millis(); - final long when = nowElapsed + mInactivityIdleThreshold; + final long inactivityThresholdMs = mIsStablePower + ? mInactivityStablePowerIdleThreshold : mInactivityIdleThreshold; + if (mIdlenessCheckScheduledElapsed >= 0) { + if (mIdlenessCheckScheduledElapsed + inactivityThresholdMs <= nowElapsed) { + if (DEBUG) { + Slog.v(TAG, "Previous idle check @ " + mIdlenessCheckScheduledElapsed + + " allows device to be idle now"); + } + handleIdleTrigger(); + return; + } + } else { + mIdlenessCheckScheduledElapsed = nowElapsed; + } + final long when = mIdlenessCheckScheduledElapsed + inactivityThresholdMs; if (DEBUG) { - Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + " when=" + when); + Slog.v(TAG, "Scheduling idle : " + reason + " now:" + nowElapsed + + " checkElapsed=" + mIdlenessCheckScheduledElapsed + " when=" + when); } mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mIdleWindowSlop, "JS idleness", @@ -232,6 +331,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private void cancelIdlenessCheck() { mAlarm.cancel(mIdleAlarmListener); + mIdlenessCheckScheduledElapsed = -1; } private void handleIdleTrigger() { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java index cdab7e538ca5fde0e0d7d25686eaf17893b15e23..92ad4dfddfc25a2bddb4a3238916e2c20020d346 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java @@ -16,9 +16,14 @@ package com.android.server.job.controllers.idle; +import android.annotation.NonNull; import android.content.Context; +import android.provider.DeviceConfig; +import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; +import com.android.server.job.JobSchedulerService; + import java.io.PrintWriter; public interface IdlenessTracker { @@ -29,7 +34,7 @@ public interface IdlenessTracker { * non-interacting state. When the idle state changes thereafter, the given * listener must be called to report the new state. */ - void startTracking(Context context, IdlenessListener listener); + void startTracking(Context context, JobSchedulerService service, IdlenessListener listener); /** * Report whether the device is currently considered "idle" for purposes of @@ -40,6 +45,12 @@ public interface IdlenessTracker { */ boolean isIdle(); + /** Process the specified constant and update internal constants if relevant. */ + void processConstant(@NonNull DeviceConfig.Properties properties, @NonNull String key); + + /** Called when the battery state changes. */ + void onBatteryStateChanged(boolean isCharging, boolean isBatteryNotLow); + /** * Dump useful information about tracked idleness-related state in plaintext. */ @@ -49,4 +60,7 @@ public interface IdlenessTracker { * Dump useful information about tracked idleness-related state to proto. */ void dump(ProtoOutputStream proto, long fieldId); + + /** Dump any internal constants the tracker may have. */ + void dumpConstants(IndentingPrintWriter pw); } diff --git a/core/api/current.txt b/core/api/current.txt index c362e951e9b75f90f168928bdab43aeda8c9829f..013010be95bed8cece03a02bd48dc315d1d5cd15 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9321,6 +9321,21 @@ package android.app.usage { field public static final int USER_INTERACTION = 7; // 0x7 } + @FlaggedApi("android.app.usage.filter_based_event_query_api") public final class UsageEventsQuery implements android.os.Parcelable { + method public int describeContents(); + method public long getBeginTimeMillis(); + method public long getEndTimeMillis(); + method @NonNull public java.util.Set<java.lang.Integer> getEventTypes(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.usage.UsageEventsQuery> CREATOR; + } + + public static final class UsageEventsQuery.Builder { + ctor public UsageEventsQuery.Builder(long, long); + method @NonNull public android.app.usage.UsageEventsQuery.Builder addEventTypes(@NonNull int...); + method @NonNull public android.app.usage.UsageEventsQuery build(); + } + public final class UsageStats implements android.os.Parcelable { ctor public UsageStats(android.app.usage.UsageStats); method public void add(android.app.usage.UsageStats); @@ -9345,6 +9360,7 @@ package android.app.usage { method public java.util.List<android.app.usage.ConfigurationStats> queryConfigurations(int, long, long); method public java.util.List<android.app.usage.EventStats> queryEventStats(int, long, long); method public android.app.usage.UsageEvents queryEvents(long, long); + method @FlaggedApi("android.app.usage.filter_based_event_query_api") @NonNull @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public android.app.usage.UsageEvents queryEvents(@NonNull android.app.usage.UsageEventsQuery); method public android.app.usage.UsageEvents queryEventsForSelf(long, long); method public java.util.List<android.app.usage.UsageStats> queryUsageStats(int, long, long); field public static final int INTERVAL_BEST = 4; // 0x4 @@ -18526,11 +18542,13 @@ package android.hardware.biometrics { public class BiometricManager { method @Deprecated @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(); method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public int canAuthenticate(int); + method @FlaggedApi("android.hardware.biometrics.last_authentication_time") @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public long getLastAuthenticationTime(int); method @NonNull @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public android.hardware.biometrics.BiometricManager.Strings getStrings(int); field public static final int BIOMETRIC_ERROR_HW_UNAVAILABLE = 1; // 0x1 field public static final int BIOMETRIC_ERROR_NONE_ENROLLED = 11; // 0xb field public static final int BIOMETRIC_ERROR_NO_HARDWARE = 12; // 0xc field public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = 15; // 0xf + field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL field public static final int BIOMETRIC_SUCCESS = 0; // 0x0 } @@ -18576,6 +18594,7 @@ package android.hardware.biometrics { field public static final int BIOMETRIC_ERROR_UNABLE_TO_PROCESS = 2; // 0x2 field public static final int BIOMETRIC_ERROR_USER_CANCELED = 10; // 0xa field public static final int BIOMETRIC_ERROR_VENDOR = 8; // 0x8 + field @FlaggedApi("android.hardware.biometrics.last_authentication_time") public static final long BIOMETRIC_NO_AUTHENTICATION = -1L; // 0xffffffffffffffffL } public abstract static class BiometricPrompt.AuthenticationCallback { @@ -18614,6 +18633,7 @@ package android.hardware.biometrics { method @Deprecated @Nullable public android.security.identity.IdentityCredential getIdentityCredential(); method @FlaggedApi("android.hardware.biometrics.add_key_agreement_crypto_object") @Nullable public javax.crypto.KeyAgreement getKeyAgreement(); method @Nullable public javax.crypto.Mac getMac(); + method @FlaggedApi("android.hardware.biometrics.get_op_id_crypto_object") public long getOpId(); method @Nullable public android.security.identity.PresentationSession getPresentationSession(); method @Nullable public java.security.Signature getSignature(); } @@ -25847,15 +25867,15 @@ package android.media.midi { method public abstract void onDisconnect(android.media.midi.MidiReceiver); } - @FlaggedApi("com.android.media.midi.flags.virtual_ump") public abstract class MidiUmpDeviceService extends android.app.Service { + @FlaggedApi("android.media.midi.virtual_ump") public abstract class MidiUmpDeviceService extends android.app.Service { ctor public MidiUmpDeviceService(); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") @Nullable public final android.media.midi.MidiDeviceInfo getDeviceInfo(); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") @NonNull public final java.util.List<android.media.midi.MidiReceiver> getOutputPortReceivers(); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") public void onClose(); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") public void onDeviceStatusChanged(@NonNull android.media.midi.MidiDeviceStatus); - method @FlaggedApi("com.android.media.midi.flags.virtual_ump") @NonNull public abstract java.util.List<android.media.midi.MidiReceiver> onGetInputPortReceivers(); - field @FlaggedApi("com.android.media.midi.flags.virtual_ump") public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; + method @FlaggedApi("android.media.midi.virtual_ump") @Nullable public final android.media.midi.MidiDeviceInfo getDeviceInfo(); + method @FlaggedApi("android.media.midi.virtual_ump") @NonNull public final java.util.List<android.media.midi.MidiReceiver> getOutputPortReceivers(); + method @FlaggedApi("android.media.midi.virtual_ump") @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); + method @FlaggedApi("android.media.midi.virtual_ump") public void onClose(); + method @FlaggedApi("android.media.midi.virtual_ump") public void onDeviceStatusChanged(@NonNull android.media.midi.MidiDeviceStatus); + method @FlaggedApi("android.media.midi.virtual_ump") @NonNull public abstract java.util.List<android.media.midi.MidiReceiver> onGetInputPortReceivers(); + field @FlaggedApi("android.media.midi.virtual_ump") public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; } } @@ -28503,12 +28523,12 @@ package android.net.vcn { method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addExposedCapability(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addGatewayOption(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig build(); - method @FlaggedApi("android.net.vcn.safe_mode_config") @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder enableSafeMode(boolean); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeExposedCapability(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeGatewayOption(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMaxMtu(@IntRange(from=0x500) int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMinUdpPort4500NatTimeoutSeconds(@IntRange(from=0x78) int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setRetryIntervalsMillis(@NonNull long[]); + method @FlaggedApi("android.net.vcn.safe_mode_config") @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setSafeModeEnabled(boolean); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setVcnUnderlyingNetworkPriorities(@NonNull java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate>); } @@ -49810,6 +49830,7 @@ package android.view { method public default void removeOnBufferTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnBufferTransformHintChangedListener); method public default void setChildBoundingInsets(@NonNull android.graphics.Rect); method public default void setTouchableRegion(@Nullable android.graphics.Region); + method @FlaggedApi("com.android.window.flags.transfer_gesture_to_embedded") public default boolean transferHostTouchGestureToEmbedded(@NonNull android.view.SurfaceControlViewHost.SurfacePackage); } @UiThread public static interface AttachedSurfaceControl.OnBufferTransformHintChangedListener { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 79cd373e0aef8d6097e0c5e4ecf3db8ba398491a..346d62bf8a30839aeae8d4c676a177b614d7c8a1 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -261,6 +261,7 @@ package android { field public static final String PROVIDE_RESOLVER_RANKER_SERVICE = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE"; field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT"; field public static final String PROVISION_DEMO_DEVICE = "android.permission.PROVISION_DEMO_DEVICE"; + field @FlaggedApi("android.content.pm.quarantined_enabled") public static final String QUARANTINE_APPS = "android.permission.QUARANTINE_APPS"; field public static final String QUERY_ADMIN_POLICY = "android.permission.QUERY_ADMIN_POLICY"; field public static final String QUERY_CLONED_APPS = "android.permission.QUERY_CLONED_APPS"; field @Deprecated public static final String QUERY_TIME_ZONE_RULES = "android.permission.QUERY_TIME_ZONE_RULES"; @@ -3861,7 +3862,7 @@ package android.content.pm { method public boolean getInstallAsVirtualPreload(); method public int getPendingUserActionReason(); method public boolean getRequestDowngrade(); - method @Nullable @RequiresPermission(android.Manifest.permission.READ_INSTALLED_SESSION_PATHS) public String getResolvedBaseApkPath(); + method @FlaggedApi("android.content.pm.get_resolved_apk_path") @Nullable @RequiresPermission(android.Manifest.permission.READ_INSTALLED_SESSION_PATHS) public String getResolvedBaseApkPath(); method public int getRollbackDataPolicy(); method @NonNull public java.util.Set<java.lang.String> getWhitelistedRestrictedPermissions(); } @@ -4089,7 +4090,7 @@ package android.content.pm { field public static final int PROTECTION_FLAG_MODULE = 4194304; // 0x400000 field public static final int PROTECTION_FLAG_OEM = 16384; // 0x4000 field public static final int PROTECTION_FLAG_RECENTS = 33554432; // 0x2000000 - field public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000 + field @Deprecated public static final int PROTECTION_FLAG_RETAIL_DEMO = 16777216; // 0x1000000 field public static final int PROTECTION_FLAG_ROLE = 67108864; // 0x4000000 field public static final int PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER = 65536; // 0x10000 field public static final int PROTECTION_FLAG_VENDOR_PRIVILEGED = 32768; // 0x8000 @@ -6750,7 +6751,7 @@ package android.media.audiopolicy { method public boolean setUidDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>); method public boolean setUserIdDeviceAffinity(int, @NonNull java.util.List<android.media.AudioDeviceInfo>); method public String toLogFriendlyString(); - method @FlaggedApi("com.android.media.audio.flags.audio_policy_update_mixing_rules_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int updateMixingRules(@NonNull java.util.List<android.util.Pair<android.media.audiopolicy.AudioMix,android.media.audiopolicy.AudioMixingRule>>); + method @FlaggedApi("android.media.audiopolicy.audio_policy_update_mixing_rules_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int updateMixingRules(@NonNull java.util.List<android.util.Pair<android.media.audiopolicy.AudioMix,android.media.audiopolicy.AudioMixingRule>>); field public static final int FOCUS_POLICY_DUCKING_DEFAULT = 0; // 0x0 field public static final int FOCUS_POLICY_DUCKING_IN_APP = 0; // 0x0 field public static final int FOCUS_POLICY_DUCKING_IN_POLICY = 1; // 0x1 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a3ebe6e7271264e26fee40b6466f1ee89a6171a8..66436633b08997e290426b1adf77c42c5fc9a8ba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -575,7 +575,6 @@ package android.app.admin { method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean setDeviceOwnerOnly(@NonNull android.content.ComponentName, int); method public void setDeviceOwnerType(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.MANAGE_DEVICE_ADMINS) public void setNextOperationSafety(int, int); - method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void setOverrideKeepProfilesRunning(boolean); method @RequiresPermission(anyOf={android.Manifest.permission.MARK_DEVICE_ORGANIZATION_OWNED, android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS}, conditional=true) public void setProfileOwnerOnOrganizationOwnedDevice(@NonNull android.content.ComponentName, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public boolean triggerDevicePolicyEngineMigration(boolean); field public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED = "android.app.action.DATA_SHARING_RESTRICTION_APPLIED"; @@ -1802,8 +1801,8 @@ package android.media { public class AudioManager { method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String); - method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean enterAudioFocusFreezeForTest(@NonNull java.util.List<java.lang.Integer>); - method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean exitAudioFocusFreezeForTest(); + method @FlaggedApi("android.media.audio.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean enterAudioFocusFreezeForTest(@NonNull java.util.List<java.lang.Integer>); + method @FlaggedApi("android.media.audio.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean exitAudioFocusFreezeForTest(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceComputeCsdOnAllDevices(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceUseFrameworkMel(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat); @@ -1811,9 +1810,9 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public float getCsd(); method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int); method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes); - method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @NonNull @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public java.util.List<java.lang.Integer> getFocusDuckedUidsForTest(); - method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusFadeOutDurationForTest(); - method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusUnmuteDelayAfterFadeOutForTest(); + method @FlaggedApi("android.media.audio.focus_freeze_test_api") @NonNull @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public java.util.List<java.lang.Integer> getFocusDuckedUidsForTest(); + method @FlaggedApi("android.media.audio.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusFadeOutDurationForTest(); + method @FlaggedApi("android.media.audio.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusUnmuteDelayAfterFadeOutForTest(); method @Nullable public static android.media.AudioHalVersionInfo getHalVersion(); method public static final int[] getPublicStreamTypes(); method @NonNull public java.util.List<java.lang.Integer> getReportedSurroundFormats(); @@ -1935,7 +1934,7 @@ package android.media { field public static final int VIBRATION_SOURCE_URI = 2; // 0x2 } - public static final class RingtoneSelection.Builder { + @FlaggedApi("android.os.vibrator.haptics_customization_enabled") public static final class RingtoneSelection.Builder { ctor public RingtoneSelection.Builder(); ctor public RingtoneSelection.Builder(@NonNull android.media.RingtoneSelection); method @NonNull public android.media.RingtoneSelection build(); @@ -3573,8 +3572,8 @@ package android.view { method public default void holdLock(android.os.IBinder, int); method public default boolean isGlobalKey(int); method public default boolean isTaskSnapshotSupported(); - method @FlaggedApi("REPLACE_CONTENT_WITH_MIRROR") @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public default boolean replaceContentOnDisplayWithMirror(int, @NonNull android.view.Window); - method @FlaggedApi("REPLACE_CONTENT_WITH_MIRROR") @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public default boolean replaceContentOnDisplayWithSc(int, @NonNull android.view.SurfaceControl); + method @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public default boolean replaceContentOnDisplayWithMirror(int, @NonNull android.view.Window); + method @RequiresPermission(android.Manifest.permission.ACCESS_SURFACE_FLINGER) public default boolean replaceContentOnDisplayWithSc(int, @NonNull android.view.SurfaceControl); method public default void setDisplayImePolicy(int, int); method public default void setShouldShowSystemDecors(int, boolean); method public default void setShouldShowWithInsecureKeyguard(int, boolean); diff --git a/core/java/android/app/BackgroundStartPrivileges.java b/core/java/android/app/BackgroundStartPrivileges.java index 76c0ccf983229724b980c4df9dbbcb80ef5d3fe8..20278eaee3b22910f8a3bd7db38cc031d408b146 100644 --- a/core/java/android/app/BackgroundStartPrivileges.java +++ b/core/java/android/app/BackgroundStartPrivileges.java @@ -174,6 +174,15 @@ public class BackgroundStartPrivileges { @Override public String toString() { + if (this == ALLOW_BAL) { + return "BSP.ALLOW_BAL"; + } + if (this == ALLOW_FGS) { + return "BSP.ALLOW_FGS"; + } + if (this == NONE) { + return "BSP.NONE"; + } return "BackgroundStartPrivileges[" + "allowsBackgroundActivityStarts=" + mAllowsBackgroundActivityStarts + ", allowsBackgroundForegroundServiceStarts=" diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 2c428efe4edbb7bfa2e4b79c96a9594e5e2e97be..1f8784bf72dd4124097bb475af6c712baff27151 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -259,12 +259,10 @@ interface IActivityTaskManager { * @param taskId the id of the task to retrieve the sAutoapshots for * @param isLowResolution if set, if the snapshot needs to be loaded from disk, this will load * a reduced resolution of it, which is much faster - * @param takeSnapshotIfNeeded if set, call {@link #takeTaskSnapshot} to trigger the snapshot - if no cache exists. * @return a graphic buffer representing a screenshot of a task */ android.window.TaskSnapshot getTaskSnapshot( - int taskId, boolean isLowResolution, boolean takeSnapshotIfNeeded); + int taskId, boolean isLowResolution); /** * Requests for a new snapshot to be taken for the task with the given id, storing it in the diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a46c1006d31478a384ca2991e803e8f367f38c85..4c70c914ff211caa49636d389a727ac106e90089 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -17047,23 +17047,6 @@ public class DevicePolicyManager { return null; } - /** - * Overrides the effective cached value of enable_keep_profiles_running for testing purposes. - * - * @hide - */ - @TestApi - @RequiresPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) - public void setOverrideKeepProfilesRunning(boolean enabled) { - if (mService != null) { - try { - mService.setOverrideKeepProfilesRunning(enabled); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - } - /** * Triggers the data migration of device policies for existing DPCs to the Device Policy Engine. * If {@code forceMigration} is set to {@code true} it skips the prerequisite checks before diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java index 8dd50f0c42e8711c99550aa3a16c763a5426f2e9..304359bc610587e8bbd91d944005bf55942ea4e3 100644 --- a/core/java/android/app/admin/DevicePolicyManagerInternal.java +++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java @@ -311,21 +311,11 @@ public abstract class DevicePolicyManagerInternal { public abstract boolean hasPermission(String callerPackage, String permission, int targetUserId); - /** - * Returns whether new "turn off work" behavior is enabled via feature flag. - */ - public abstract boolean isKeepProfilesRunningEnabled(); - /** * True if either the entire device or the user is organization managed. */ public abstract boolean isUserOrganizationManaged(@UserIdInt int userId); - /** - * Returns the list of packages suspended by admin on a given user. - */ - public abstract Set<String> getPackagesSuspendedByAdmin(@UserIdInt int userId); - /** * Returns whether the application exemptions feature flag is enabled. */ diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 58f9d57763face29200833ed7b3bf447608c783c..6fe40be041cc3c54ffdaa087d78b431079bd2866 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -603,8 +603,6 @@ interface IDevicePolicyManager { DevicePolicyState getDevicePolicyState(); - void setOverrideKeepProfilesRunning(boolean enabled); - boolean triggerDevicePolicyEngineMigration(boolean forceMigration); boolean isDeviceFinanced(String callerPackageName); diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 49543a1e3d994a2b8047aa3048950d166d409d06..ebd5d649fc2bc92a85a1ac85e5c83e186308de9a 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -20,10 +20,9 @@ import android.app.PendingIntent; import android.app.usage.BroadcastResponseStats; import android.app.usage.BroadcastResponseStatsList; import android.app.usage.UsageEvents; +import android.app.usage.UsageEventsQuery; import android.content.pm.ParceledListSlice; -import java.util.Map; - /** * System private API for talking with the UsageStatsManagerService. * @@ -42,6 +41,8 @@ interface IUsageStatsManager { UsageEvents queryEventsForPackage(long beginTime, long endTime, String callingPackage); UsageEvents queryEventsForUser(long beginTime, long endTime, int userId, String callingPackage); UsageEvents queryEventsForPackageForUser(long beginTime, long endTime, int userId, String pkg, String callingPackage); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") + UsageEvents queryEventsWithFilter(in UsageEventsQuery query, String callingPackage); @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) void setAppInactive(String packageName, boolean inactive, int userId); boolean isAppStandbyEnabled(); diff --git a/core/java/android/app/usage/UsageEvents.java b/core/java/android/app/usage/UsageEvents.java index c188686d2d5b0d4e1f9133c41db5bc534178b938..1eb452cfd085113ec5bc49101a5d04ba971f2872 100644 --- a/core/java/android/app/usage/UsageEvents.java +++ b/core/java/android/app/usage/UsageEvents.java @@ -349,6 +349,47 @@ public final class UsageEvents implements Parcelable { */ public static final int MAX_EVENT_TYPE = 31; + /** + * Keep in sync with the event types defined above. + * @hide + */ + @IntDef(flag = false, value = { + NONE, + ACTIVITY_RESUMED, + ACTIVITY_PAUSED, + END_OF_DAY, + CONTINUE_PREVIOUS_DAY, + CONFIGURATION_CHANGE, + SYSTEM_INTERACTION, + USER_INTERACTION, + SHORTCUT_INVOCATION, + CHOOSER_ACTION, + NOTIFICATION_SEEN, + STANDBY_BUCKET_CHANGED, + NOTIFICATION_INTERRUPTION, + SLICE_PINNED_PRIV, + SLICE_PINNED, + SCREEN_INTERACTIVE, + SCREEN_NON_INTERACTIVE, + KEYGUARD_SHOWN, + KEYGUARD_HIDDEN, + FOREGROUND_SERVICE_START, + FOREGROUND_SERVICE_STOP, + CONTINUING_FOREGROUND_SERVICE, + ROLLOVER_FOREGROUND_SERVICE, + ACTIVITY_STOPPED, + ACTIVITY_DESTROYED, + FLUSH_TO_DISK, + DEVICE_SHUTDOWN, + DEVICE_STARTUP, + USER_UNLOCKED, + USER_STOPPED, + LOCUS_ID_SET, + APP_COMPONENT_USED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EventType {} + /** @hide */ public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0; diff --git a/core/java/android/app/usage/UsageEventsQuery.aidl b/core/java/android/app/usage/UsageEventsQuery.aidl new file mode 100644 index 0000000000000000000000000000000000000000..5ed370ddfa86dbe75800058dacb3344a593e0190 --- /dev/null +++ b/core/java/android/app/usage/UsageEventsQuery.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.usage; + +parcelable UsageEventsQuery; \ No newline at end of file diff --git a/core/java/android/app/usage/UsageEventsQuery.java b/core/java/android/app/usage/UsageEventsQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..8c63d1857865af7fca08f497807fc052dad87ffe --- /dev/null +++ b/core/java/android/app/usage/UsageEventsQuery.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.usage; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.app.usage.UsageEvents.Event; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArraySet; + +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * An Object-Oriented representation for a {@link UsageEvents} query. + * Used by {@link UsageStatsManager#queryEvents(UsageEventsQuery)} call. + */ +@FlaggedApi(Flags.FLAG_FILTER_BASED_EVENT_QUERY_API) +public final class UsageEventsQuery implements Parcelable { + private final @CurrentTimeMillisLong long mBeginTimeMillis; + private final @CurrentTimeMillisLong long mEndTimeMillis; + private final @Event.EventType int[] mEventTypes; + + private UsageEventsQuery(@NonNull Builder builder) { + mBeginTimeMillis = builder.mBeginTimeMillis; + mEndTimeMillis = builder.mEndTimeMillis; + mEventTypes = ArrayUtils.convertToIntArray(builder.mEventTypes); + } + + private UsageEventsQuery(Parcel in) { + mBeginTimeMillis = in.readLong(); + mEndTimeMillis = in.readLong(); + int eventTypesLength = in.readInt(); + mEventTypes = new int[eventTypesLength]; + in.readIntArray(mEventTypes); + } + + /** + * Returns the inclusive timestamp to indicate the beginning of the range of events. + * Defined in terms of "Unix time", see {@link java.lang.System#currentTimeMillis}. + */ + public @CurrentTimeMillisLong long getBeginTimeMillis() { + return mBeginTimeMillis; + } + + /** + * Returns the exclusive timpstamp to indicate the end of the range of events. + * Defined in terms of "Unix time", see {@link java.lang.System#currentTimeMillis}. + */ + public @CurrentTimeMillisLong long getEndTimeMillis() { + return mEndTimeMillis; + } + + /** + * Returns the set of usage event types for the query. + * <em>Note: An empty set indicates query for all usage events. </em> + */ + public @NonNull Set<Integer> getEventTypes() { + if (ArrayUtils.isEmpty(mEventTypes)) { + return Collections.emptySet(); + } + + HashSet<Integer> eventTypeSet = new HashSet<>(); + for (int eventType : mEventTypes) { + eventTypeSet.add(eventType); + } + return eventTypeSet; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mBeginTimeMillis); + dest.writeLong(mEndTimeMillis); + dest.writeInt(mEventTypes.length); + dest.writeIntArray(mEventTypes); + } + + @NonNull + public static final Creator<UsageEventsQuery> CREATOR = + new Creator<UsageEventsQuery>() { + @Override + public UsageEventsQuery createFromParcel(Parcel in) { + return new UsageEventsQuery(in); + } + + @Override + public UsageEventsQuery[] newArray(int size) { + return new UsageEventsQuery[size]; + } + }; + + /** @hide */ + public int[] getEventTypeFilter() { + return Arrays.copyOf(mEventTypes, mEventTypes.length); + } + + /** + * Builder for UsageEventsQuery. + */ + public static final class Builder { + private final @CurrentTimeMillisLong long mBeginTimeMillis; + private final @CurrentTimeMillisLong long mEndTimeMillis; + private final ArraySet<Integer> mEventTypes = new ArraySet<>(); + + /** + * Constructor that specifies the period for which to return events. + * @param beginTimeMillis Inclusive beginning timestamp, as per + * {@link java.lang.System#currentTimeMillis()} + * @param endTimeMillis Exclusive ending timestamp, as per + * {@link java.lang.System#currentTimeMillis()} + * + * @throws IllegalArgumentException if {@code beginTimeMillis} < + * {@code endTimeMillis} + */ + public Builder(@CurrentTimeMillisLong long beginTimeMillis, + @CurrentTimeMillisLong long endTimeMillis) { + if (beginTimeMillis < 0 || endTimeMillis < beginTimeMillis) { + throw new IllegalArgumentException("Invalid period"); + } + mBeginTimeMillis = beginTimeMillis; + mEndTimeMillis = endTimeMillis; + } + + /** + * Builds a read-only UsageEventsQuery object. + */ + public @NonNull UsageEventsQuery build() { + return new UsageEventsQuery(this); + } + + /** + * Specifies the list of usage event types to be included in the query. + * @param eventTypes List of the usage event types. See {@link UsageEvents.Event} + * + * @throws llegalArgumentException if the event type is not valid. + */ + public @NonNull Builder addEventTypes(@NonNull @Event.EventType int... eventTypes) { + for (int i = 0; i < eventTypes.length; i++) { + final int eventType = eventTypes[i]; + if (eventType < Event.NONE || eventType > Event.MAX_EVENT_TYPE) { + throw new IllegalArgumentException("Invalid usage event type: " + eventType); + } + mEventTypes.add(eventType); + } + return this; + } + } +} diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 2a10ed181576a1fa64d618f028134482d2028d70..4f1c993bfa1a72c529e3e822c1e551bf577acea7 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -18,6 +18,7 @@ package android.app.usage; import android.Manifest; import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -580,6 +581,29 @@ public final class UsageStatsManager { return sEmptyResults; } + /** + * Query for events with specific UsageEventsQuery object. + * <em>Note: if the user's device is not in an unlocked state (as defined by + * {@link UserManager#isUserUnlocked()}), then {@code null} will be returned.</em> + * + * @param query The query object used to specify the query parameters. + * @return A {@link UsageEvents}. + */ + @FlaggedApi(Flags.FLAG_FILTER_BASED_EVENT_QUERY_API) + @NonNull + @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) + public UsageEvents queryEvents(@NonNull UsageEventsQuery query) { + try { + UsageEvents iter = mService.queryEventsWithFilter(query, mContext.getOpPackageName()); + if (iter != null) { + return iter; + } + } catch (RemoteException e) { + // fallthrough and return empty result. + } + return sEmptyResults; + } + /** * Like {@link #queryEvents(long, long)}, but only returns events for the calling package. * <em>Note: Starting from {@link android.os.Build.VERSION_CODES#R Android R}, if the user's diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index 0b8e29f954a554cb8e02cd79028386c12e1253ea..a611255c38176f35228a70cd2896ae4d424e6f38 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -28,3 +28,10 @@ flag { description: "Flag for parcelable usage event list" bug: "301254110" } + +flag { + name: "filter_based_event_query_api" + namespace: "backstage_power" + description: " Feature flag to support filter based event query API" + bug: "194321117" +} diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 1114b358e08aea1c7bb7faa13e312874df632a2c..6681e54beaa1981a4525b37deb845a51a952e0a3 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -3832,6 +3832,7 @@ public class PackageInstaller { */ @SystemApi @RequiresPermission(Manifest.permission.READ_INSTALLED_SESSION_PATHS) + @FlaggedApi(Flags.FLAG_GET_RESOLVED_APK_PATH) public @Nullable String getResolvedBaseApkPath() { return resolvedBaseCodePath; } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 36433ce846181cb8a63ee6cd110d0a85c6ae4e5a..e2a5747a4bcb21ea286449d52d58f74a3ca3b01d 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1480,6 +1480,7 @@ public abstract class PackageManager { INSTALL_ALLOW_DOWNGRADE, INSTALL_STAGED, INSTALL_REQUEST_UPDATE_OWNERSHIP, + INSTALL_IGNORE_DEXOPT_PROFILE, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -1711,6 +1712,18 @@ public abstract class PackageManager { */ public static final int INSTALL_ARCHIVED = 1 << 27; + /** + * If set, all dexopt profiles are ignored by dexopt during the installation, including the + * profile in the DM file and the profile embedded in the APK file. If an invalid profile is + * provided during installation, no warning will be reported by {@code adb install}. + * + * This option does not affect later dexopt operations (e.g., background dexopt and manual `pm + * compile` invocations). + * + * @hide + */ + public static final int INSTALL_IGNORE_DEXOPT_PROFILE = 1 << 28; + /** * Flag parameter for {@link #installPackage} to force a non-staged update of an APEX. This is * a development-only feature and should not be used on end user devices. diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java index cdda12eebdc440abcdf1d4f3dcd4bb00f0e0937e..012b6c49f4c5339a490f1c482d524cf3fac019eb 100644 --- a/core/java/android/content/pm/PermissionInfo.java +++ b/core/java/android/content/pm/PermissionInfo.java @@ -273,6 +273,9 @@ public class PermissionInfo extends PackageItemInfo implements Parcelable { * to the <code>retailDemo</code> value of * {@link android.R.attr#protectionLevel}. * + * @deprecated This flag has been replaced by the retail demo role and is a no-op since Android + * V. + * * @hide */ @SystemApi diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index a8fe21ebfbbda4fcc258fd9808b5decd008d081f..814eae6726a90e4b0f3a66f5eff6c593292b6971 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -60,7 +60,6 @@ flag { } flag { - name: "rollback_lifetime" namespace: "package_manager_service" description: "Feature flag to enable custom rollback lifetime during install." @@ -74,3 +73,10 @@ flag { description: "Feature flag to improve install freeze time." bug: "307561242" } + +flag { + name: "get_resolved_apk_path" + namespace: "package_manager_service" + description: "Feature flag to retrieve resolved path of the base APK during an app install." + bug: "269728874" +} diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java index 943eee461809581b732797d71d5fd4ce09847a71..61d87026b6e9a896d2c1b61f6d5a921e434fa2a0 100644 --- a/core/java/android/hardware/biometrics/BiometricConstants.java +++ b/core/java/android/hardware/biometrics/BiometricConstants.java @@ -18,6 +18,7 @@ package android.hardware.biometrics; import static android.hardware.biometrics.BiometricManager.Authenticators; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; @@ -296,4 +297,15 @@ public interface BiometricConstants { @Retention(RetentionPolicy.SOURCE) @IntDef({BIOMETRIC_LOCKOUT_NONE, BIOMETRIC_LOCKOUT_TIMED, BIOMETRIC_LOCKOUT_PERMANENT}) @interface LockoutMode {} + + // + // Other miscellaneous constants + // + + /** + * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when there has + * been no successful authentication for the given authenticator since boot. + */ + @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) + long BIOMETRIC_NO_AUTHENTICATION = -1; } diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java index 82694ee3463b438add4c8bf545c276fad49fa332..90bbca8336e14216b27c3b4d66dfae4bc5aa4051 100644 --- a/core/java/android/hardware/biometrics/BiometricManager.java +++ b/core/java/android/hardware/biometrics/BiometricManager.java @@ -23,6 +23,8 @@ import static android.Manifest.permission.WRITE_DEVICE_CONFIG; import static com.android.internal.util.FrameworkStatsLog.AUTH_DEPRECATED_APIUSED__DEPRECATED_API__API_BIOMETRIC_MANAGER_CAN_AUTHENTICATE; +import android.annotation.ElapsedRealtimeLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,6 +32,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.KeyguardManager; import android.content.Context; import android.os.IBinder; import android.os.RemoteException; @@ -85,6 +88,17 @@ public class BiometricManager { public static final int BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED = BiometricConstants.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED; + /** + * Returned from {@link BiometricManager#getLastAuthenticationTime(int)} when no matching + * successful authentication has been performed since boot. + */ + @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) + public static final long BIOMETRIC_NO_AUTHENTICATION = + BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; + + private static final int GET_LAST_AUTH_TIME_ALLOWED_AUTHENTICATORS = + Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_STRONG; + /** * @hide */ @@ -637,5 +651,58 @@ public class BiometricManager { } } + + /** + * Gets the last time the user successfully authenticated using one of the given authenticators. + * The returned value is time in + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} (time since + * boot, including sleep). + * <p> + * {@link BiometricManager#BIOMETRIC_NO_AUTHENTICATION} is returned in the case where there + * has been no successful authentication using any of the given authenticators since boot. + * <p> + * Currently, only {@link Authenticators#DEVICE_CREDENTIAL} and + * {@link Authenticators#BIOMETRIC_STRONG} are accepted. {@link IllegalArgumentException} will + * be thrown if {@code authenticators} contains other authenticator types. + * <p> + * Note that this may return successful authentication times even if the device is currently + * locked. You may use {@link KeyguardManager#isDeviceLocked()} to determine if the device + * is unlocked or not. Additionally, this method may return valid times for an authentication + * method that is no longer available. For instance, if the user unlocked the device with a + * {@link Authenticators#BIOMETRIC_STRONG} authenticator but then deleted that authenticator + * (e.g., fingerprint data), this method will still return the time of that unlock for + * {@link Authenticators#BIOMETRIC_STRONG} if it is the most recent successful event. The caveat + * is that {@link BiometricManager#BIOMETRIC_NO_AUTHENTICATION} will be returned if the device + * no longer has a secure lock screen at all, even if there were successful authentications + * performed before the lock screen was made insecure. + * + * @param authenticators bit field consisting of constants defined in {@link Authenticators}. + * @return the time of last authentication or + * {@link BiometricManager#BIOMETRIC_NO_AUTHENTICATION} + * @throws IllegalArgumentException if {@code authenticators} contains values other than + * {@link Authenticators#DEVICE_CREDENTIAL} and {@link Authenticators#BIOMETRIC_STRONG} or is + * 0. + */ + @RequiresPermission(USE_BIOMETRIC) + @ElapsedRealtimeLong + @FlaggedApi(Flags.FLAG_LAST_AUTHENTICATION_TIME) + public long getLastAuthenticationTime( + @BiometricManager.Authenticators.Types int authenticators) { + if (authenticators == 0 + || (GET_LAST_AUTH_TIME_ALLOWED_AUTHENTICATORS & authenticators) != authenticators) { + throw new IllegalArgumentException( + "Only BIOMETRIC_STRONG and DEVICE_CREDENTIAL authenticators may be used."); + } + + if (mService != null) { + try { + return mService.getLastAuthenticationTime(UserHandle.myUserId(), authenticators); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } else { + return BIOMETRIC_NO_AUTHENTICATION; + } + } } diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 7a43286692c44f5b4725377ffdeb9fe3c095ee23..97bbfbbe49084194a3dd88d0ab7f73bfc2f28a59 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -21,6 +21,7 @@ import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT; +import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; @@ -851,6 +852,14 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public @Nullable KeyAgreement getKeyAgreement() { return super.getKeyAgreement(); } + + /** + * Get the operation handle associated with this object or 0 if none. + */ + @FlaggedApi(FLAG_GET_OP_ID_CRYPTO_OBJECT) + public long getOpId() { + return super.getOpId(); + } } /** diff --git a/core/java/android/hardware/biometrics/CryptoObject.java b/core/java/android/hardware/biometrics/CryptoObject.java index 39fbe83b6abbd96bd66e4a714bba974ac66351d3..8d3ea3f914ec6e6f02d330df4e6d627582491d20 100644 --- a/core/java/android/hardware/biometrics/CryptoObject.java +++ b/core/java/android/hardware/biometrics/CryptoObject.java @@ -154,7 +154,7 @@ public class CryptoObject { * @hide * @return the opId associated with this object or 0 if none */ - public final long getOpId() { + public long getOpId() { if (mCrypto == null) { return 0; } else if (mCrypto instanceof IdentityCredential) { diff --git a/core/java/android/hardware/biometrics/IAuthService.aidl b/core/java/android/hardware/biometrics/IAuthService.aidl index c2e5c0b6d51925ec48e51f28e4634fbab26cf829..5bdbe2b573b2f474a285bed42778f6ad0aa22d0c 100644 --- a/core/java/android/hardware/biometrics/IAuthService.aidl +++ b/core/java/android/hardware/biometrics/IAuthService.aidl @@ -57,6 +57,9 @@ interface IAuthService { // Checks if biometrics can be used. int canAuthenticate(String opPackageName, int userId, int authenticators); + // Gets the time of last authentication for the given user and authenticators. + long getLastAuthenticationTime(int userId, int authenticators); + // Checks if any biometrics are enrolled. boolean hasEnrolledBiometrics(int userId, String opPackageName); diff --git a/core/java/android/hardware/biometrics/IBiometricService.aidl b/core/java/android/hardware/biometrics/IBiometricService.aidl index 18c8d1bd3a1e3ae23d6388f11c46f575d7126175..058f302af62b200dc2c2e4b3ee1e34b38f50c9c8 100644 --- a/core/java/android/hardware/biometrics/IBiometricService.aidl +++ b/core/java/android/hardware/biometrics/IBiometricService.aidl @@ -53,6 +53,10 @@ interface IBiometricService { @EnforcePermission("USE_BIOMETRIC_INTERNAL") int canAuthenticate(String opPackageName, int userId, int callingUserId, int authenticators); + // Gets the time of last authentication for the given user and authenticators. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + long getLastAuthenticationTime(int userId, int authenticators); + // Checks if any biometrics are enrolled. @EnforcePermission("USE_BIOMETRIC_INTERNAL") boolean hasEnrolledBiometrics(int userId, String opPackageName); diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 66429e5046d89c9cbcf60d280a81c5bc8dc7464a..56ec7632fa20769ece767c1350c9874a2aa142b1 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -1,5 +1,12 @@ package: "android.hardware.biometrics" +flag { + name: "last_authentication_time" + namespace: "wallet_integration" + description: "Feature flag for adding getLastAuthenticationTime API to BiometricManager" + bug: "301979982" +} + flag { name: "add_key_agreement_crypto_object" namespace: "biometrics" @@ -7,3 +14,10 @@ flag { bug: "282058146" } +flag { + name: "get_op_id_crypto_object" + namespace: "biometrics" + description: "Feature flag for adding a get operation id api to CryptoObject." + bug: "307601768" +} + diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java index 779a8db433a8fc9f4c98585ea2eb42ef8c1bc124..6f11d3ae661c49c46c07beccb112976451cd7a1f 100644 --- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java +++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java @@ -493,7 +493,7 @@ public final class VcnGatewayConnectionConfig { /** * Check whether safe mode is enabled * - * @see Builder#enableSafeMode(boolean) + * @see Builder#setSafeModeEnabled(boolean) */ @FlaggedApi(FLAG_SAFE_MODE_CONFIG) public boolean isSafeModeEnabled() { @@ -815,8 +815,8 @@ public final class VcnGatewayConnectionConfig { * * <p>If a VCN fails to provide connectivity within a system-provided timeout, it will enter * safe mode. In safe mode, the VCN Network will be torn down and the system will restore - * connectivity by allowing underlying cellular networks to be used as default. At the same - * time, VCN will continue to retry until it succeeds. + * connectivity by allowing underlying cellular or WiFi networks to be used as default. At + * the same time, VCN will continue to retry until it succeeds. * * <p>When safe mode is disabled and VCN connection fails to provide connectivity, end users * might not have connectivity, and may not have access to carrier-owned underlying @@ -826,7 +826,7 @@ public final class VcnGatewayConnectionConfig { */ @FlaggedApi(FLAG_SAFE_MODE_CONFIG) @NonNull - public Builder enableSafeMode(boolean enabled) { + public Builder setSafeModeEnabled(boolean enabled) { mIsSafeModeDisabled = !enabled; return this; } diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index a3b836adfc8b7cec4e0572c04761cf0377ad3325..d4688f8794a499b92ed4893d9311fd8030260a3c 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -933,6 +933,12 @@ public class RecoverySystem { rebootWipeUserData(context, shutdown, reason, force, false /* wipeEuicc */); } + /** {@hide} */ + public static void rebootWipeUserData(Context context, boolean shutdown, String reason, + boolean force, boolean wipeEuicc) throws IOException { + rebootWipeUserData(context, shutdown, reason, force, wipeEuicc, false /* keepMemtagMode */); + } + /** * Reboots the device and wipes the user data and cache * partitions. This is sometimes called a "factory reset", which @@ -948,6 +954,7 @@ public class RecoverySystem { * @param force whether the {@link UserManager.DISALLOW_FACTORY_RESET} user restriction * should be ignored * @param wipeEuicc whether wipe the euicc data + * @param keepMemtagMode whether to tell recovery to keep currently configured memtag mode * * @throws IOException if writing the recovery command file * fails, or if the reboot itself fails. @@ -956,7 +963,7 @@ public class RecoverySystem { * @hide */ public static void rebootWipeUserData(Context context, boolean shutdown, String reason, - boolean force, boolean wipeEuicc) throws IOException { + boolean force, boolean wipeEuicc, boolean keepMemtagMode) throws IOException { UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE); if (!force && um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) { throw new SecurityException("Wiping data is not allowed for this user."); @@ -996,8 +1003,13 @@ public class RecoverySystem { reasonArg = "--reason=" + sanitizeArg(reason + "," + timeStamp); } + String memtagArg = null; + if (keepMemtagMode) { + memtagArg = "--keep_memtag_mode"; + } + final String localeArg = "--locale=" + Locale.getDefault().toLanguageTag() ; - bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg); + bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg, memtagArg); } /** diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 9f931b4059c86ddc241dd5482f7e24e6e9b032c3..c012ff34bfabfb2b12b291113ecadc59382fa8ce 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15279,6 +15279,7 @@ public final class Settings { * max_history_files (int) * max_history_buffer_kb (int) * battery_charged_delay_ms (int) + * battery_charging_enforce_level (int) * </pre> * * <p> @@ -18306,8 +18307,11 @@ public final class Settings { /** * Delay for sending ACTION_CHARGING after device is plugged in. - * This is used as an override for constants defined in BatteryStatsImpl for - * ease of experimentation. + * This is used as an override for constants defined in BatteryStatsImpl. Its purposes are: + * <ol> + * <li>Ease of experimentation</li> + * <li>Customization of different device</li> + * </ol> * * @see com.android.internal.os.BatteryStatsImpl.Constants.KEY_BATTERY_CHARGED_DELAY_MS * @hide @@ -18316,6 +18320,22 @@ public final class Settings { public static final String BATTERY_CHARGING_STATE_UPDATE_DELAY = "battery_charging_state_update_delay"; + /** + * Threshold battery level to enforce battery state as charging. That means when battery + * level is equal to or higher than this threshold, it is always considered charging, even + * if battery level lowered. + * This is used as an override for constants defined in BatteryStatsImpl. Its purposes are: + * <ol> + * <li>Ease of experimentation</li> + * <li>Customization of different device</li> + * </ol> + * + * @hide + * @see com.android.internal.os.BatteryStatsImpl.Constants.BATTERY_CHARGING_ENFORCE_LEVEL + */ + public static final String BATTERY_CHARGING_STATE_ENFORCE_LEVEL = + "battery_charging_state_enforce_level"; + /** * A serialized string of params that will be loaded into a text classifier action model. * diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index 52d4d47007a3843ae8557db220fd2bef0f008b98..2a05c847f40464ad3aaad6e4a4df70d6a1dd6035 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -4,7 +4,7 @@ flag { name: "ranking_update_ashmem" namespace: "systemui" description: "This flag controls moving ranking update contents into ashmem" - bug: "284297289" + bug: "249848655" } flag { diff --git a/core/java/android/speech/tts/BlockingAudioTrack.java b/core/java/android/speech/tts/BlockingAudioTrack.java index be5851c3e64f6f7817c2c22f7114caa5a03be3cb..d84cc2c60c3b4ffc63931e4dcc90992802e478cb 100644 --- a/core/java/android/speech/tts/BlockingAudioTrack.java +++ b/core/java/android/speech/tts/BlockingAudioTrack.java @@ -194,17 +194,22 @@ class BlockingAudioTrack { audioTrack.play(); } - int count = 0; - while (count < bytes.length) { - // Note that we don't take bufferCopy.mOffset into account because - // it is guaranteed to be 0. - int written = audioTrack.write(bytes, count, bytes.length); + int offset = 0; + while (offset < bytes.length) { + // Although it requests to write the entire bytes at once, it might fail when the track + // got stopped or the thread is interrupted. In that case, it needs to carry on from + // last offset. + int sizeToWrite = bytes.length - offset; + int written = audioTrack.write(bytes, offset, sizeToWrite); if (written <= 0) { + if (written < 0) { + Log.e(TAG, "An error occurred while writing to audio track: " + written); + } break; } - count += written; + offset += written; } - return count; + return offset; } private AudioTrack createStreamingAudioTrack() { diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index d5457511876b1cb28ab8133a07b4134146432e65..fd5517d29d74a4cb803948b9eb502892e14cf12c 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -231,4 +231,19 @@ public interface AttachedSurfaceControl { default void removeTrustedPresentationCallback(@NonNull SurfaceControl.Transaction t, @NonNull Consumer<Boolean> listener) { } + + /** + * Transfer the currently in progress touch gesture from the host to the requested + * {@link SurfaceControlViewHost.SurfacePackage}. This requires that the + * SurfaceControlViewHost was created with the current host's inputToken. + * + * @param surfacePackage The SurfacePackage to transfer the gesture to. + * @return Whether the touch stream was transferred. + */ + @FlaggedApi(Flags.FLAG_TRANSFER_GESTURE_TO_EMBEDDED) + default boolean transferHostTouchGestureToEmbedded( + @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) { + throw new UnsupportedOperationException( + "transferHostTouchGestureToEmbedded is unimplemented"); + } } diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 7acf2f8ce06d3821b399c88305e9d98bee51975b..02e97dae70e29d0da6fd9834f710631df3c26401 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -357,4 +357,6 @@ interface IWindowSession { boolean cancelDraw(IWindow window); boolean transferEmbeddedTouchFocusToHost(IWindow embeddedWindow); + + boolean transferHostTouchGestureToEmbedded(IWindow hostWindow, IBinder transferTouchToken); } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 9d88af95ade8a829d4b7ea645fcfcd7ce1ba7a86..451a71b71e3faaa9ef19142c51e60a7a049cece8 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -2671,7 +2671,7 @@ public final class SurfaceControl implements Parcelable { * * @param minAlpha The min alpha the {@link SurfaceControl} is required to * have to be considered inside the threshold. - * @param minFractionRendered The min fraction of the SurfaceControl that was resented + * @param minFractionRendered The min fraction of the SurfaceControl that was presented * to the user to be considered inside the threshold. * @param stabilityRequirementMs The time in milliseconds required for the * {@link SurfaceControl} to be in the threshold. diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index 57b19a8ce12f951f02d320dc3dc0c5bda865cbbb..405653123f793c7ab117e69872138b7b68af1073 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -287,7 +287,8 @@ public class SurfaceControlViewHost { } /** - * Returns an input token used which can be used to request focus on the embedded surface. + * Returns an input token used which can be used to request focus on the embedded surface + * or to transfer touch gesture to the embedded surface. * * @hide */ @@ -526,7 +527,8 @@ public class SurfaceControlViewHost { } /** - * Returns an input token used which can be used to request focus on the embedded surface. + * Returns an input token used which can be used to request focus on the embedded surface + * or to transfer touch gesture to the embedded surface. * * @hide */ diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 24223694e42172b7297554f263b4f7376f98f43d..1ee303ccdd026b36c6ff65e3becc597aed4aa032 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11893,4 +11893,17 @@ public final class ViewRootImpl implements ViewParent, } Log.d(mTag, msg); } + + @Override + public boolean transferHostTouchGestureToEmbedded( + @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) { + final IWindowSession realWm = WindowManagerGlobal.getWindowSession(); + try { + return realWm.transferHostTouchGestureToEmbedded(mWindow, + surfacePackage.getInputToken()); + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + return false; + } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 4f03ce98ccace057779b26c1d50d968642a292ec..cfec08120776a441cea5c4206073742771a93dbd 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -90,6 +90,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; @@ -5872,7 +5873,7 @@ public interface WindowManager extends ViewManager { * * @hide */ - @FlaggedApi("REPLACE_CONTENT_WITH_MIRROR") + @SuppressLint("UnflaggedApi") // The API is only used for tests. @TestApi @RequiresPermission(permission.ACCESS_SURFACE_FLINGER) default boolean replaceContentOnDisplayWithMirror(int displayId, @NonNull Window window) { @@ -5888,7 +5889,7 @@ public interface WindowManager extends ViewManager { * * @hide */ - @FlaggedApi("REPLACE_CONTENT_WITH_MIRROR") + @SuppressLint("UnflaggedApi") // The API is only used for tests. @TestApi @RequiresPermission(permission.ACCESS_SURFACE_FLINGER) default boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) { diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 7c3b6ae42fcf8bc3af4744df1af51ce955a3fd67..652fe2445ddc0719ace185559749d1cb1758f44d 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -656,6 +656,14 @@ public class WindowlessWindowManager implements IWindowSession { return false; } + @Override + public boolean transferHostTouchGestureToEmbedded(IWindow hostWindow, + IBinder embeddedInputToken) { + Log.e(TAG, "Received request to transferHostTouchGestureToEmbedded on" + + " WindowlessWindowManager. We shouldn't get here!"); + return false; + } + void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) { IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder(); IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder(); diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 5ad5c79f2d42394504cf33bc84959a04e70178f9..68eddff19922b9c5f5114d6cd06641b3cd7772d3 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -25,3 +25,10 @@ flag { is_fixed_read_only: true bug: "304508760" } + +flag { + namespace: "window_surfaces" + name: "transfer_gesture_to_embedded" + description: "Enable public API for Window Surfaces" + bug: "287076178" +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c75f99616f5b29096772bb109f25b70f5ba65460..ec302e7f0147f6d36309ab35e8be2d9e15764ed0 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2358,6 +2358,15 @@ <permission android:name="android.permission.SUSPEND_APPS" android:protectionLevel="signature|role" /> + <!-- @SystemApi + @hide + @FlaggedApi("android.content.pm.quarantined_enabled") + Allows an application to quarantine other apps, which will prevent + them from running without explicit user action. + --> + <permission android:name="android.permission.QUARANTINE_APPS" + android:protectionLevel="internal|verifier" /> + <!-- Allows applications to discover and pair bluetooth devices. <p>Protection level: normal --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 95f14931c6ef3655557a23f74fd0556e67129daf..9f99dc94bc92fefa904904dd7f4eee52ad26fc73 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -301,7 +301,9 @@ granted to the system companion device manager service --> <flag name="companion" value="0x800000" /> <!-- Additional flag from base permission type: this permission will be granted to the - retail demo app, as defined by the OEM. --> + retail demo app, as defined by the OEM. + This flag has been replaced by the retail demo role and is a no-op since Android V. + --> <flag name="retailDemo" value="0x1000000" /> <!-- Additional flag from base permission type: this permission will be granted to the recents app. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index dbce054dd01ac7e499d2f6006d89d4de962cfdcc..862e537aa8c91d77a4feab2a1e2a2c6da1792218 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4177,6 +4177,10 @@ <!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider the device to be "idle" after being inactive for this long. --> <integer name="config_jobSchedulerInactivityIdleThreshold">1860000</integer> + <!-- Inactivity threshold (in milliseconds) used in JobScheduler. JobScheduler will consider + the device to be "idle" after being inactive for this long if the device is on stable + power. Stable power is defined as "charging + battery not low". --> + <integer name="config_jobSchedulerInactivityIdleThresholdOnStablePower">1860000</integer> <!-- The alarm window (in milliseconds) that JobScheduler uses to enter the idle state --> <integer name="config_jobSchedulerIdleWindowSlop">300000</integer> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a2f0086491a1407465fcf4370744fd09e9d749f1..e646548b8ccbcec9824683088800674027fc5ed0 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2882,6 +2882,7 @@ <java-symbol type="integer" name="config_defaultNightMode" /> <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThreshold" /> + <java-symbol type="integer" name="config_jobSchedulerInactivityIdleThresholdOnStablePower" /> <java-symbol type="integer" name="config_jobSchedulerIdleWindowSlop" /> <java-symbol type="bool" name="config_jobSchedulerRestrictBackgroundUser" /> <java-symbol type="integer" name="config_jobSchedulerUserGracePeriod" /> diff --git a/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java b/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..839b645cf3525cb8a0103dcef2aca01a2610258d --- /dev/null +++ b/core/tests/coretests/src/android/app/usage/UsageEventsQueryTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app.usage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.app.usage.UsageEvents.Event; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Random; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UsageEventsQueryTest { + @Test + public void testQueryDuration() { + // Test with negative beginTimeMillis. + long beginTimeMillis = -100; + long endTimeMillis = 100; + try { + UsageEventsQuery query = new UsageEventsQuery.Builder(beginTimeMillis, endTimeMillis) + .build(); + fail("beginTimeMillis should be a non-negative timestamp measured as the number of" + + " milliseconds since 1970-01-01T00:00:00Z."); + } catch (IllegalArgumentException e) { + // Expected, fall through; + } + + // Test with negative endTimeMillis. + beginTimeMillis = 1001; + endTimeMillis = -1; + try { + UsageEventsQuery query = new UsageEventsQuery.Builder(beginTimeMillis, endTimeMillis) + .build(); + fail("endTimeMillis should be a non-negative timestamp measured as the number of" + + " milliseconds since 1970-01-01T00:00:00Z."); + } catch (IllegalArgumentException e) { + // Expected, fall through; + } + + // Test with beginTimeMillis < endTimeMillis; + beginTimeMillis = 2001; + endTimeMillis = 1000; + try { + UsageEventsQuery query = new UsageEventsQuery.Builder(beginTimeMillis, endTimeMillis) + .build(); + fail("beginTimeMillis should be smaller than endTimeMillis"); + } catch (IllegalArgumentException e) { + // Expected, fall through; + } + + // Test with beginTimeMillis == endTimeMillis, valid. + beginTimeMillis = 1001; + endTimeMillis = 1001; + try { + UsageEventsQuery query = new UsageEventsQuery.Builder(beginTimeMillis, endTimeMillis) + .build(); + assertEquals(query.getBeginTimeMillis(), query.getEndTimeMillis()); + } catch (IllegalArgumentException e) { + // Not expected for valid duration. + fail("Valid duration for beginTimeMillis=" + beginTimeMillis + + ", endTimeMillis=" + endTimeMillis); + } + + beginTimeMillis = 2001; + endTimeMillis = 3001; + try { + UsageEventsQuery query = new UsageEventsQuery.Builder(beginTimeMillis, endTimeMillis) + .build(); + assertEquals(query.getBeginTimeMillis(), 2001); + assertEquals(query.getEndTimeMillis(), 3001); + } catch (IllegalArgumentException e) { + // Not expected for valid duration. + fail("Valid duration for beginTimeMillis=" + beginTimeMillis + + ", endTimeMillis=" + endTimeMillis); + } + } + + @Test + public void testQueryEventTypes() { + Random rnd = new Random(); + UsageEventsQuery.Builder queryBuilder = new UsageEventsQuery.Builder(1000, 2000); + + // Test with invalid event type. + int eventType = Event.NONE - 1; + try { + queryBuilder.addEventTypes(eventType); + fail("Invalid event type: " + eventType); + } catch (IllegalArgumentException e) { + // Expected, fall through. + } + + eventType = Event.MAX_EVENT_TYPE + 1; + try { + queryBuilder.addEventTypes(eventType); + fail("Invalid event type: " + eventType); + } catch (IllegalArgumentException e) { + // Expected, fall through. + } + + // Test with valid and duplicate event types. + eventType = rnd.nextInt(Event.MAX_EVENT_TYPE + 1); + try { + UsageEventsQuery query = queryBuilder.addEventTypes(eventType, eventType, eventType) + .build(); + Set<Integer> eventTypeSet = query.getEventTypes(); + assertEquals(eventTypeSet.size(), 1); + int type = eventTypeSet.iterator().next(); + assertEquals(type, eventType); + } catch (IllegalArgumentException e) { + fail("Valid event type: " + eventType); + } + } +} diff --git a/keystore/java/android/security/Authorization.java b/keystore/java/android/security/Authorization.java index 2d2dd24763c4ffcf56d920c6a1b7d3260676434b..b4b3e92750354da48d1d1b01a2575a7afbdaf985 100644 --- a/keystore/java/android/security/Authorization.java +++ b/keystore/java/android/security/Authorization.java @@ -18,7 +18,9 @@ package android.security; import android.annotation.NonNull; import android.annotation.Nullable; +import android.hardware.biometrics.BiometricConstants; import android.hardware.security.keymint.HardwareAuthToken; +import android.hardware.security.keymint.HardwareAuthenticatorType; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; @@ -37,7 +39,10 @@ public class Authorization { public static final int SYSTEM_ERROR = ResponseCode.SYSTEM_ERROR; - private static IKeystoreAuthorization getService() { + /** + * @return an instance of IKeystoreAuthorization + */ + public static IKeystoreAuthorization getService() { return IKeystoreAuthorization.Stub.asInterface( ServiceManager.checkService("android.security.authorization")); } @@ -100,4 +105,24 @@ public class Authorization { } } + /** + * Gets the last authentication time of the given user and authenticators. + * + * @param userId user id + * @param authenticatorTypes an array of {@link HardwareAuthenticatorType}. + * @return the last authentication time or + * {@link BiometricConstants#BIOMETRIC_NO_AUTHENTICATION}. + */ + public static long getLastAuthenticationTime( + long userId, @HardwareAuthenticatorType int[] authenticatorTypes) { + try { + return getService().getLastAuthTime(userId, authenticatorTypes); + } catch (RemoteException | NullPointerException e) { + Log.w(TAG, "Can not connect to keystore", e); + return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; + } catch (ServiceSpecificException e) { + return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; + } + } + } diff --git a/keystore/java/android/security/GateKeeper.java b/keystore/java/android/security/GateKeeper.java index af188a95c92904c0cebb40dbff644ff0a87f3ede..464714fe2895f11d893e8f62af3f4f813f05ae6d 100644 --- a/keystore/java/android/security/GateKeeper.java +++ b/keystore/java/android/security/GateKeeper.java @@ -45,8 +45,19 @@ public abstract class GateKeeper { @UnsupportedAppUsage public static long getSecureUserId() throws IllegalStateException { + return getSecureUserId(UserHandle.myUserId()); + } + + /** + * Return the secure user id for a given user id + * @param userId the user id, e.g. 0 + * @return the secure user id or {@link GateKeeper#INVALID_SECURE_USER_ID} if no such mapping + * for the given user id is found. + * @throws IllegalStateException if there is an error retrieving the secure user id + */ + public static long getSecureUserId(int userId) throws IllegalStateException { try { - return getService().getSecureUserId(UserHandle.myUserId()); + return getService().getSecureUserId(userId); } catch (RemoteException e) { throw new IllegalStateException( "Failed to obtain secure user ID from gatekeeper", e); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index a663f9fafb502ee34517cb585938e9a0e5af4566..ed99501b867d34d7364a54896cd18b6f682b1254 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -20,6 +20,7 @@ import android.app.ActivityTaskManager; import android.app.ActivityThread; import android.app.Application; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -40,12 +41,17 @@ import java.util.Objects; */ public class WindowExtensionsImpl implements WindowExtensions { + private static final String TAG = "WindowExtensionsImpl"; private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; private volatile WindowLayoutComponentImpl mWindowLayoutComponent; private volatile SplitController mSplitController; private volatile WindowAreaComponent mWindowAreaComponent; + public WindowExtensionsImpl() { + Log.i(TAG, "Initializing Window Extensions."); + } + // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 76f0b6769855d1588308cf2bb7f801ee7df4e33a..4973a4d85af7fb2dc171b7a51b351f5bc366128d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -156,6 +156,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent, @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { + Log.i(TAG, "Initializing Activity Embedding Controller."); final MainThreadExecutor executor = new MainThreadExecutor(); mHandler = executor.mHandler; mPresenter = new SplitPresenter(executor, windowLayoutComponent, this); @@ -208,6 +209,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { synchronized (mLock) { + Log.i(TAG, "Setting embedding rules. Size: " + rules.size()); mSplitRules.clear(); mSplitRules.addAll(rules); } @@ -216,6 +218,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) { synchronized (mLock) { + Log.i(TAG, "Request to pin top activity stack."); final TaskContainer task = getTaskContainer(taskId); if (task == null) { Log.e(TAG, "Cannot find the task for id: " + taskId); @@ -272,6 +275,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void unpinTopActivityStack(int taskId){ synchronized (mLock) { + Log.i(TAG, "Request to unpin top activity stack."); final TaskContainer task = getTaskContainer(taskId); if (task == null) { Log.e(TAG, "Cannot find the task to unpin, id: " + taskId); 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 84feb03e6a402edf00694c8c38d360f0c4aa0287..108aa82750090a834760a658e12197d1ac238d42 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 @@ -129,9 +129,7 @@ object PipUtils { @JvmStatic fun getTaskSnapshot(taskId: Int, isLowResolution: Boolean): TaskSnapshot? { return if (taskId <= 0) null else try { - ActivityTaskManager.getService().getTaskSnapshot( - taskId, isLowResolution, false /* takeSnapshotIfNeeded */ - ) + ActivityTaskManager.getService().getTaskSnapshot(taskId, isLowResolution) } catch (e: RemoteException) { Log.e(TAG, "Failed to get task snapshot, taskId=$taskId", e) null 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 ea7b2e92fefb713e818d6fff280f6e293202060d..64294c9dbdc6cca7ae8101b032713147cb5dd0e5 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 @@ -103,6 +103,7 @@ import com.android.wm.shell.sysui.ShellInterface; import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.taskview.TaskViewFactoryController; import com.android.wm.shell.taskview.TaskViewTransitions; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; @@ -613,14 +614,22 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + HomeTransitionObserver homeTransitionObserver) { if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { // TODO(b/238217847): Force override shell init if registration is disabled shellInit = new ShellInit(mainExecutor); } return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - rootTaskDisplayAreaOrganizer); + rootTaskDisplayAreaOrganizer, homeTransitionObserver); + } + + @WMSingleton + @Provides + static HomeTransitionObserver provideHomeTransitionObserver(Context context, + @ShellMainThread ShellExecutor mainExecutor) { + return new HomeTransitionObserver(context, mainExecutor); } @WMSingleton 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 a533ca5fa8fb09ef7b8699f724c6f2ad1017ff65..47769a8eeb11cc388ab055801e4f3574b1bbb549 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 @@ -78,6 +78,7 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.DefaultMixedHandler; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; import com.android.wm.shell.unfold.UnfoldAnimationController; @@ -380,9 +381,10 @@ public abstract class WMShellModule { static RecentsTransitionHandler provideRecentsTransitionHandler( ShellInit shellInit, Transitions transitions, - Optional<RecentTasksController> recentTasksController) { + Optional<RecentTasksController> recentTasksController, + HomeTransitionObserver homeTransitionObserver) { return new RecentsTransitionHandler(shellInit, transitions, - recentTasksController.orElse(null)); + recentTasksController.orElse(null), homeTransitionObserver); } // 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 412a5b5a6997a73dc307cb88a81e5ba903aeb9ba..8e12991080edbb57e42714027b328f1b38b7c33b 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 @@ -343,9 +343,8 @@ class DesktopTasksController( task.taskId ) val wct = WindowContainerTransaction() - wct.setWindowingMode(task.token, WINDOWING_MODE_MULTI_WINDOW) wct.setBounds(task.token, Rect()) - wct.setDensityDpi(task.token, getDefaultDensityDpi()) + addMoveToSplitChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) } else { @@ -827,7 +826,9 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) + // Explicitly setting multi-window at task level interferes with animations. + // Let task inherit windowing mode once transition is complete instead. + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED) // The task's density may have been overridden in freeform; revert it here as we don't // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index d277eef761e9b575ed2fc7a84e2d97ad2ebecdd2..c20d23e4374e701deba4ff019d21725d84b9dee7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -59,6 +59,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.util.TransitionUtil; @@ -85,11 +86,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { */ private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>(); + private final HomeTransitionObserver mHomeTransitionObserver; + public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions, - @Nullable RecentTasksController recentTasksController) { + @Nullable RecentTasksController recentTasksController, + HomeTransitionObserver homeTransitionObserver) { mTransitions = transitions; mExecutor = transitions.getMainExecutor(); mRecentTasksController = recentTasksController; + mHomeTransitionObserver = homeTransitionObserver; if (!Transitions.ENABLE_SHELL_TRANSITIONS) return; if (recentTasksController == null) return; shellInit.addInitCallback(() -> { @@ -911,6 +916,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { Slog.e(TAG, "Duplicate call to finish"); return; } + if (!toHome) { + // For some transitions, we may have notified home activity that it became visible. + // We need to notify the observer that we are no longer going home. + mHomeTransitionObserver.notifyHomeVisibilityChanged(false); + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: toHome=%b userLeave=%b " + "willFinishToHome=%b state=%d", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index f561aa5568beec15b8c12fd403f2c62ecc1cb167..b528089d153ed4137bfcf12c7634d3d891fabb87 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -38,24 +38,15 @@ import com.android.wm.shell.util.TransitionUtil; */ public class HomeTransitionObserver implements TransitionObserver, RemoteCallable<HomeTransitionObserver> { - private final SingleInstanceRemoteListener<HomeTransitionObserver, IHomeTransitionListener> + private SingleInstanceRemoteListener<HomeTransitionObserver, IHomeTransitionListener> mListener; private @NonNull final Context mContext; private @NonNull final ShellExecutor mMainExecutor; - private @NonNull final Transitions mTransitions; - public HomeTransitionObserver(@NonNull Context context, - @NonNull ShellExecutor mainExecutor, - @NonNull Transitions transitions) { + @NonNull ShellExecutor mainExecutor) { mContext = context; mMainExecutor = mainExecutor; - mTransitions = transitions; - - mListener = new SingleInstanceRemoteListener<>(this, - c -> mTransitions.registerObserver(this), - c -> mTransitions.unregisterObserver(this)); - } @Override @@ -72,7 +63,7 @@ public class HomeTransitionObserver implements TransitionObserver, final int mode = change.getMode(); if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME && TransitionUtil.isOpenOrCloseMode(mode)) { - mListener.call(l -> l.onHomeVisibilityChanged(TransitionUtil.isOpeningType(mode))); + notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode)); } } } @@ -92,7 +83,14 @@ public class HomeTransitionObserver implements TransitionObserver, * Sets the home transition listener that receives any transitions resulting in a change of * */ - public void setHomeTransitionListener(IHomeTransitionListener listener) { + public void setHomeTransitionListener(Transitions transitions, + IHomeTransitionListener listener) { + if (mListener == null) { + mListener = new SingleInstanceRemoteListener<>(this, + c -> transitions.registerObserver(this), + c -> transitions.unregisterObserver(this)); + } + if (listener != null) { mListener.register(listener); } else { @@ -100,6 +98,16 @@ public class HomeTransitionObserver implements TransitionObserver, } } + /** + * Notifies the listener that the home visibility has changed. + * @param isVisible true when home activity is visible, false otherwise. + */ + public void notifyHomeVisibilityChanged(boolean isVisible) { + if (mListener != null) { + mListener.call(l -> l.onHomeVisibilityChanged(isVisible)); + } + } + @Override public Context getContext() { return mContext; @@ -113,7 +121,7 @@ public class HomeTransitionObserver implements TransitionObserver, /** * Invalidates this controller, preventing future calls to send updates. */ - public void invalidate() { - mTransitions.unregisterObserver(this); + public void invalidate(Transitions transitions) { + transitions.unregisterObserver(this); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 718c704f2c65e440991c1a4835f7418b7e9850f7..ab5c0636f2b576d76f7021c13d05339ddfc4259f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -192,6 +192,8 @@ public class Transitions implements RemoteCallable<Transitions>, private final ArrayList<TransitionObserver> mObservers = new ArrayList<>(); + private HomeTransitionObserver mHomeTransitionObserver; + /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>(); @@ -267,10 +269,11 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull DisplayController displayController, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, - @NonNull ShellExecutor animExecutor) { + @NonNull ShellExecutor animExecutor, + @NonNull HomeTransitionObserver observer) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, - new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit)); + new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer); } public Transitions(@NonNull Context context, @@ -283,7 +286,8 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) { + @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, + @NonNull HomeTransitionObserver observer) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; @@ -302,6 +306,7 @@ public class Transitions implements RemoteCallable<Transitions>, mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); + mHomeTransitionObserver = observer; } private void onInit() { @@ -351,7 +356,7 @@ public class Transitions implements RemoteCallable<Transitions>, } private ExternalInterfaceBinder createExternalInterface() { - return new IShellTransitionsImpl(mContext, getMainExecutor(), this); + return new IShellTransitionsImpl(this); } @Override @@ -1397,12 +1402,9 @@ public class Transitions implements RemoteCallable<Transitions>, private static class IShellTransitionsImpl extends IShellTransitions.Stub implements ExternalInterfaceBinder { private Transitions mTransitions; - private final HomeTransitionObserver mHomeTransitionObserver; - IShellTransitionsImpl(Context context, ShellExecutor executor, Transitions transitions) { + IShellTransitionsImpl(Transitions transitions) { mTransitions = transitions; - mHomeTransitionObserver = new HomeTransitionObserver(context, executor, - mTransitions); } /** @@ -1410,7 +1412,7 @@ public class Transitions implements RemoteCallable<Transitions>, */ @Override public void invalidate() { - mHomeTransitionObserver.invalidate(); + mTransitions.mHomeTransitionObserver.invalidate(mTransitions); mTransitions = null; } @@ -1440,7 +1442,8 @@ public class Transitions implements RemoteCallable<Transitions>, public void setHomeTransitionListener(IHomeTransitionListener listener) { executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener", (transitions) -> { - mHomeTransitionObserver.setHomeTransitionListener(listener); + transitions.mHomeTransitionObserver.setHomeTransitionListener(mTransitions, + listener); }); } } 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 716f8b81f15869869f4b2951e89d283fd8e44bd4..e206039aa6bf7cf40e6e6c05158e9d6eca685b57 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 @@ -82,6 +82,7 @@ import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -238,7 +239,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController = splitScreenController; mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() { @Override - public void onTaskStageChanged(int taskId, int stage, boolean visible) { + public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) { if (visible) { DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId); if (decor != null && DesktopModeStatus.isEnabled() @@ -391,10 +392,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (id == R.id.close_window) { - mTaskOperations.closeTask(mTaskToken); if (isTaskInSplitScreen(mTaskId)) { - RunningTaskInfo remainingTask = getOtherSplitTask(mTaskId); - mSplitScreenController.moveTaskToFullscreen(remainingTask.taskId); + mSplitScreenController.moveTaskToFullscreen(getOtherSplitTask(mTaskId).taskId); + } else { + mTaskOperations.closeTask(mTaskToken); } } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(); @@ -417,8 +418,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } decoration.closeHandleMenu(); } else if (id == R.id.fullscreen_button) { - mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId)); decoration.closeHandleMenu(); + if (isTaskInSplitScreen(mTaskId)) { + mSplitScreenController.moveTaskToFullscreen(mTaskId); + } else { + mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId)); + } } else if (id == R.id.split_screen_button) { decoration.closeHandleMenu(); mDesktopTasksController.ifPresent(c -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt index 9dc86db4f59bb2c2aee1222cef8852e9458d83a1..b1fb0f184bffbc2241541a7b3757b0b685690cb0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt @@ -42,11 +42,11 @@ internal class DesktopModeFocusedWindowDecorationViewHolder( } override fun onHandleMenuOpened() { - animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) + animateCaptionHandleAlpha(startValue = 1f, endValue = 0f) } override fun onHandleMenuClosed() { - animateCaptionHandleAlpha(startValue = 1f, endValue = 0f) + animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml index 1df11369a0490f7d7c3a0e8c93af3d3f7ba16238..5b2ffec67e930f4037dad975f1fba24fd94b5fbf 100644 --- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml @@ -95,14 +95,6 @@ <option name="pull-pattern-keys" value="perfetto_file_path"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 1df11369a0490f7d7c3a0e8c93af3d3f7ba16238..9f7d9fcf1326a07905ba33dd4eaaa48da268490a 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -93,16 +93,8 @@ <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml index 1df11369a0490f7d7c3a0e8c93af3d3f7ba16238..882b200da3a2bac686bc1e084382b237db63a04b 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml @@ -93,16 +93,10 @@ <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> + value="/data/user/0/com.android.wm.shell.flicker.pip.apps/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml index ca182fa5a2667f3c2414402ae62769be4faea6b2..6df65391ea617a0dc23bdb047908305d29c37053 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml @@ -57,6 +57,17 @@ <option name="test-file-name" value="WMShellFlickerTestsPipAppsCSuite.apk"/> <option name="test-file-name" value="FlickerTestApp.apk"/> </target_preparer> + + <!-- Needed for installing apk's from Play Store --> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="aapt-version" value="AAPT2"/> + <option name="throw-if-not-found" value="false"/> + <option name="install-arg" value="-d"/> + <option name="install-arg" value="-g"/> + <option name="install-arg" value="-r"/> + <option name="test-file-name" value="pstash://com.netflix.mediaclient"/> + </target_preparer> + <!-- Enable mocking GPS location by the test app --> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="run-command" @@ -93,29 +104,9 @@ <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.pip.apps/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> - - <!-- Needed for installing apk's from Play Store --> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> - <option name="aapt-version" value="AAPT2"/> - <option name="throw-if-not-found" value="false"/> - <option name="install-arg" value="-d"/> - <option name="install-arg" value="-g"/> - <option name="install-arg" value="-r"/> - <option name="test-file-name" value="pstash://com.netflix.mediaclient"/> - </target_preparer> </configuration> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt index be5a27ac7dcc90aeb8e3c1a8593975f5e4283c17..bd8b0056a6a3599ea2f4705e4c0530ed8794c277 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt @@ -20,6 +20,8 @@ import android.platform.test.annotations.Postsubmit import android.tools.common.Rotation import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.apphelpers.StandardAppHelper +import android.tools.device.flicker.junit.FlickerBuilderProvider +import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import com.android.wm.shell.flicker.pip.common.EnterPipTransition @@ -29,6 +31,15 @@ import org.junit.runners.Parameterized abstract class AppsEnterPipTransition(flicker: LegacyFlickerTest) : EnterPipTransition(flicker) { protected abstract val standardAppHelper: StandardAppHelper + @FlickerBuilderProvider + override fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + withoutScreenRecorder() + setup { flicker.scenario.setIsTablet(tapl.isTablet) } + transition() + } + } + /** Checks [standardAppHelper] window remains visible throughout the animation */ @Postsubmit @Test diff --git a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml index 1df11369a0490f7d7c3a0e8c93af3d3f7ba16238..51a55e359acf62580b8dc5d97244ac2b338d63db 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml @@ -93,14 +93,6 @@ <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml index 1df11369a0490f7d7c3a0e8c93af3d3f7ba16238..fdda5974d1f99fc71df507aea157b7e3f472a7fd 100644 --- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml @@ -93,16 +93,8 @@ <!-- Needed for pulling the collected trace config on to the host --> <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.pip/files"/> <option name="directory-keys" value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker.service/files"/> <option name="collect-on-run-ended-only" value="true"/> <option name="clean-up" value="true"/> </metrics_collector> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt index 0f3e0f5ef043e720f039e487b59a328769ecf824..e9363f725fd800adb0fef6a78a3b8ac504c50e68 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseBenchmarkTest.kt @@ -38,7 +38,7 @@ constructor( * executions */ @FlickerBuilderProvider - fun buildFlicker(): FlickerBuilder { + open fun buildFlicker(): FlickerBuilder { return FlickerBuilder(instrumentation).apply { setup { flicker.scenario.setIsTablet(tapl.isTablet) } transition() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt index c31b9e2c22c7eac6089f1d1d21d78cd6f733a466..3244ebc9ef3225edf74468461b6fa7852cd2da1b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt @@ -135,19 +135,29 @@ object SplitScreenUtils { // second task to split. val home = tapl.workspace.switchToOverview() ChangeDisplayOrientationRule.setRotation(rotation) - home.overviewActions.clickSplit() + val isGridOnlyOverviewEnabled = tapl.isGridOnlyOverviewEnabled + if (isGridOnlyOverviewEnabled) { + home.currentTask.tapMenu().tapSplitMenuItem() + } else { + home.overviewActions.clickSplit() + } val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS) if (snapshots == null || snapshots.size < 1) { error("Fail to find a overview snapshot to split.") } - // Find the second task in the upper right corner in split select mode by sorting - // 'left' in descending order and 'top' in ascending order. + // Find the second task in the upper (or bottom for grid only Overview) right corner in + // split select mode by sorting 'left' in descending order and 'top' in ascending (or + // descending for grid only Overview) order. snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> t2.getVisibleBounds().left - t1.getVisibleBounds().left } snapshots.sortWith { t1: UiObject2, t2: UiObject2 -> - t1.getVisibleBounds().top - t2.getVisibleBounds().top + if (isGridOnlyOverviewEnabled) { + t2.getVisibleBounds().top - t1.getVisibleBounds().top + } else { + t1.getVisibleBounds().top - t2.getVisibleBounds().top + } } snapshots[0].click() } else { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index cc4db22e772c57b246a61d9f4d707a92e7ca7b0a..fff65f364121111f1b4941bbe9feacacb42db261 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -74,6 +74,7 @@ import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -373,7 +374,7 @@ public class StageCoordinatorTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 7a8a2a93e0e1b9ac0483311266e1b942293b6b91..ea7c0d9c264ea5ec86e78a6752fdd7a656a2baca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -75,24 +75,24 @@ public class HomeTransitionObserverTest extends ShellTestCase { private final Handler mMainHandler = new Handler(Looper.getMainLooper()); private final DisplayController mDisplayController = mock(DisplayController.class); + private IHomeTransitionListener mListener; private Transitions mTransition; + private HomeTransitionObserver mHomeTransitionObserver; @Before public void setUp() { + mListener = mock(IHomeTransitionListener.class); + when(mListener.asBinder()).thenReturn(mock(IBinder.class)); + + mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor); mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class), mOrganizer, mTransactionPool, mDisplayController, mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mHomeTransitionObserver); + mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener); } @Test public void testHomeActivityWithOpenModeNotifiesHomeIsVisible() throws RemoteException { - IHomeTransitionListener listener = mock(IHomeTransitionListener.class); - when(listener.asBinder()).thenReturn(mock(IBinder.class)); - - HomeTransitionObserver observer = new HomeTransitionObserver(mContext, mMainExecutor, - mTransition); - observer.setHomeTransitionListener(listener); - TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); @@ -101,23 +101,16 @@ public class HomeTransitionObserverTest extends ShellTestCase { setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN); - observer.onTransitionReady(mock(IBinder.class), + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); - verify(listener, times(1)).onHomeVisibilityChanged(true); + verify(mListener, times(1)).onHomeVisibilityChanged(true); } @Test public void testHomeActivityWithCloseModeNotifiesHomeIsNotVisible() throws RemoteException { - IHomeTransitionListener listener = mock(IHomeTransitionListener.class); - when(listener.asBinder()).thenReturn(mock(IBinder.class)); - - HomeTransitionObserver observer = new HomeTransitionObserver(mContext, mMainExecutor, - mTransition); - observer.setHomeTransitionListener(listener); - TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); @@ -126,38 +119,30 @@ public class HomeTransitionObserverTest extends ShellTestCase { setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_TO_BACK); - observer.onTransitionReady(mock(IBinder.class), + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); - verify(listener, times(1)).onHomeVisibilityChanged(false); + verify(mListener, times(1)).onHomeVisibilityChanged(false); } @Test public void testNonHomeActivityDoesNotTriggerCallback() throws RemoteException { - IHomeTransitionListener listener = mock(IHomeTransitionListener.class); - when(listener.asBinder()).thenReturn(mock(IBinder.class)); - - HomeTransitionObserver observer = new HomeTransitionObserver(mContext, mMainExecutor, - mTransition); - observer.setHomeTransitionListener(listener); - TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); when(change.getTaskInfo()).thenReturn(taskInfo); when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); - setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_UNDEFINED, TRANSIT_TO_BACK); - observer.onTransitionReady(mock(IBinder.class), + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), info, mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class)); - verify(listener, times(0)).onHomeVisibilityChanged(anyBoolean()); + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); } /** diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index da83d4c0a1222dc35799254729ba44d71309c737..4e300d9a7e69a0e1ae44fe001652142d2f538fc3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -146,7 +146,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = mock(ShellInit.class); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); // One from Transitions, one from RootTaskDisplayAreaOrganizer verify(shellInit).addInitCallback(any(), eq(t)); verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class)); @@ -158,7 +158,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellController shellController = mock(ShellController.class); final Transitions t = new Transitions(mContext, shellInit, shellController, mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); shellInit.init(); verify(shellController, times(1)).addExternalInterface( eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any()); @@ -1075,10 +1075,10 @@ public class ShellTransitionTests extends ShellTestCase { final Transitions transitions = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); final RecentsTransitionHandler recentsHandler = new RecentsTransitionHandler(shellInit, transitions, - mock(RecentTasksController.class)); + mock(RecentTasksController.class), mock(HomeTransitionObserver.class)); transitions.replaceDefaultHandlerForTest(mDefaultHandler); shellInit.init(); @@ -1623,7 +1623,7 @@ public class ShellTransitionTests extends ShellTestCase { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor, - mMainHandler, mAnimExecutor); + mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class)); shellInit.init(); return t; } diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index abd9284866071c47b6133713dcc47f8f6925f432..576ebc1579ef1c676768c744fd800f53563a5397 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -24,6 +24,7 @@ #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> @@ -34,6 +35,8 @@ #define INDENT2 " " #define INDENT3 " " +namespace input_flags = com::android::input::flags; + namespace android { namespace { @@ -63,10 +66,20 @@ void PointerController::DisplayInfoListener::onPointerControllerDestroyed() { std::shared_ptr<PointerController> PointerController::create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled) { + SpriteController& spriteController, bool enabled, ControllerType type) { // using 'new' to access non-public constructor - std::shared_ptr<PointerController> controller = std::shared_ptr<PointerController>( - new PointerController(policy, looper, spriteController, enabled)); + std::shared_ptr<PointerController> controller; + switch (type) { + case ControllerType::MOUSE: + controller = std::shared_ptr<PointerController>( + new MousePointerController(policy, looper, spriteController, enabled)); + break; + case ControllerType::LEGACY: + default: + controller = std::shared_ptr<PointerController>( + new PointerController(policy, looper, spriteController, enabled)); + break; + } /* * Now we need to hook up the constructed PointerController object to its callbacks. @@ -375,4 +388,13 @@ std::string PointerController::dump() { return dump; } +// --- MousePointerController --- + +MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy, + const sp<Looper>& looper, + SpriteController& spriteController, bool enabled) + : PointerController(policy, looper, spriteController, enabled) { + PointerController::setPresentation(Presentation::POINTER); +} + } // namespace android diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index aa7ca3c52ecfc56eb8ae823a67d00005e8cc9b04..08e19a096d87e6c19985722019231e5767df4fd6 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -47,7 +47,8 @@ class PointerController : public PointerControllerInterface { public: static std::shared_ptr<PointerController> create( const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, - SpriteController& spriteController, bool enabled); + SpriteController& spriteController, bool enabled, + ControllerType type = ControllerType::LEGACY); ~PointerController() override; @@ -75,7 +76,7 @@ public: void onDisplayInfosChangedLocked(const std::vector<gui::DisplayInfo>& displayInfos) REQUIRES(getLock()); - std::string dump(); + std::string dump() override; protected: using WindowListenerConsumer = @@ -87,10 +88,10 @@ protected: WindowListenerConsumer registerListener, WindowListenerConsumer unregisterListener); -private: PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, bool enabled); +private: friend PointerControllerContext::LooperCallback; friend PointerControllerContext::MessageHandler; @@ -135,6 +136,24 @@ private: void clearSpotsLocked() REQUIRES(getLock()); }; +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); + + void setPresentation(Presentation) override { + LOG_ALWAYS_FATAL("Should not be called"); + } + void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override { + LOG_ALWAYS_FATAL("Should not be called"); + } + void clearSpots() override { + LOG_ALWAYS_FATAL("Should not be called"); + } +}; + } // namespace android #endif // _UI_POINTER_CONTROLLER_H diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index eea635771dc350430670d3b73c8847b6a4f9f257..5d211f460cc514befc562625456e00f4c47eeb1f 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -20,8 +20,8 @@ import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAUL import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; -import static com.android.media.audio.flags.Flags.autoPublicVolumeApiHardening; -import static com.android.media.audio.flags.Flags.FLAG_FOCUS_FREEZE_TEST_API; +import static android.media.audio.Flags.autoPublicVolumeApiHardening; +import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; import android.Manifest; import android.annotation.CallbackExecutor; @@ -686,6 +686,7 @@ public class AudioManager { FLAG_ABSOLUTE_VOLUME, }) @Retention(RetentionPolicy.SOURCE) + // TODO(308698465) remove due to potential conflict with the new flags class public @interface Flags {} /** diff --git a/media/java/android/media/RingtoneSelection.java b/media/java/android/media/RingtoneSelection.java index b74b6a3dbac921433b333d1f8ac017c717576489..b7c3721824647166b817757a6d4e6176a21184f0 100644 --- a/media/java/android/media/RingtoneSelection.java +++ b/media/java/android/media/RingtoneSelection.java @@ -642,6 +642,7 @@ public final class RingtoneSelection { * allowing the user to configure their selection. Once a selection is stored as a Uri, then * the RingtoneSelection can be loaded directly using {@link RingtoneSelection#fromUri}. */ + @FlaggedApi(Flags.FLAG_HAPTICS_CUSTOMIZATION_ENABLED) public static final class Builder { private Uri mSoundUri; private Uri mVibrationUri; diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 9ced2a45a6f79a8b64dd6cda5725e91fe1fe3796..e16849811b9de669453491d3fe6ef8bc8f494825 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -49,7 +49,6 @@ import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; -import com.android.media.audio.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/media/java/android/media/midi/MidiUmpDeviceService.java b/media/java/android/media/midi/MidiUmpDeviceService.java index c54bfce840aabbdb1cf810e0d065f665b47bb8be..bbbef4755dc55ed2c66051c61ba22f9e370ef4e5 100644 --- a/media/java/android/media/midi/MidiUmpDeviceService.java +++ b/media/java/android/media/midi/MidiUmpDeviceService.java @@ -16,8 +16,6 @@ package android.media.midi; -import static com.android.media.midi.flags.Flags.FLAG_VIRTUAL_UMP; - import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -57,11 +55,11 @@ import java.util.List; * android:resource="@xml/device_info" /> * </service></pre> */ -@FlaggedApi(FLAG_VIRTUAL_UMP) +@FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public abstract class MidiUmpDeviceService extends Service { private static final String TAG = "MidiUmpDeviceService"; - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService"; private IMidiManager mMidiManager; @@ -80,7 +78,7 @@ public abstract class MidiUmpDeviceService extends Service { } }; - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) @Override public void onCreate() { mMidiManager = IMidiManager.Stub.asInterface( @@ -118,7 +116,7 @@ public abstract class MidiUmpDeviceService extends Service { * The number of input and output ports must be equal and non-zero. * @return list of MidiReceivers */ - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public abstract @NonNull List<MidiReceiver> onGetInputPortReceivers(); /** @@ -127,7 +125,7 @@ public abstract class MidiUmpDeviceService extends Service { * The number of input and output ports must be equal and non-zero. * @return the list of MidiReceivers */ - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public final @NonNull List<MidiReceiver> getOutputPortReceivers() { if (mServer == null) { return new ArrayList<MidiReceiver>(); @@ -140,7 +138,7 @@ public abstract class MidiUmpDeviceService extends Service { * Returns the {@link MidiDeviceInfo} instance for this service * @return the MidiDeviceInfo of the virtual MIDI device if it was successfully created */ - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public final @Nullable MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; } @@ -149,7 +147,7 @@ public abstract class MidiUmpDeviceService extends Service { * Called to notify when the {@link MidiDeviceStatus} has changed * @param status the current status of the MIDI device */ - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public void onDeviceStatusChanged(@NonNull MidiDeviceStatus status) { } @@ -157,11 +155,11 @@ public abstract class MidiUmpDeviceService extends Service { * Called to notify when the virtual MIDI device running in this service has been closed by * all its clients */ - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) public void onClose() { } - @FlaggedApi(FLAG_VIRTUAL_UMP) + @FlaggedApi(Flags.FLAG_VIRTUAL_UMP) @Override public @Nullable IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt index 5d6aa03ef8b33fccadcaa21b835fc92ed71445e8..d763f77d2644814fcc2a7c4db847448d750ec79d 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPageModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,6 @@ import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.common.PageModel import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.util.getIntArg import com.android.settingslib.spa.framework.util.getStringArg import com.android.settingslib.spa.framework.util.navLink @@ -110,7 +109,7 @@ class ArgumentPageModel : PageModel() { fun genStringParamPreferenceModel(): PreferenceModel { return object : PreferenceModel { override val title = STRING_PARAM_TITLE - override val summary = stateOf(stringParam!!) + override val summary = { stringParam!! } } } @@ -118,7 +117,7 @@ class ArgumentPageModel : PageModel() { fun genIntParamPreferenceModel(): PreferenceModel { return object : PreferenceModel { override val title = INT_PARAM_TITLE - override val summary = stateOf(intParam!!.toString()) + override val summary = { intParam!!.toString() } } } @@ -130,7 +129,7 @@ class ArgumentPageModel : PageModel() { ) return object : PreferenceModel { override val title = PAGE_TITLE - override val summary = stateOf(summaryArray.joinToString(", ")) + override val summary = { summaryArray.joinToString(", ") } override val onClick = navigator( SettingsPageProviderEnum.ARGUMENT.name + parameter.navLink(arguments) ) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt index 50c0eb70bb037404f7191acf2cb0f8ee7c16a033..345b47aff7910f4c7d8a9ce2113da809708b89b0 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPageProvider.kt @@ -26,7 +26,6 @@ import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.widget.preference.Preference @@ -50,7 +49,7 @@ object FooterPageProvider : SettingsPageProvider { Preference(remember { object : PreferenceModel { override val title = "Some Preference" - override val summary = stateOf("Some summary") + override val summary = { "Some summary" } } }) }.build() diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt index 43b6d0b05696cfd06091c877a9a45ed81bc188e9..d7de9b4f20453c2e59d0af8fa1c5d29e4a2ab795 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/ListPreferencePageProvider.kt @@ -99,7 +99,7 @@ private fun SampleNotChangeableListPreference() { ListPreference(remember { object : ListPreferenceModel { override val title = "Preferred network type" - override val enabled = enabled + override val enabled = { enabled.value } override val options = listOf( ListPreferenceOption(id = 1, text = "5G (recommended)"), ListPreferenceOption(id = 2, text = "LTE"), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt similarity index 95% rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt index 238204a66bb4ee065a50a89194ef4385fb9560ba..96de1a778c9792ef5bd37db8df9e9d12bdc5c20b 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferencePageProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,6 @@ import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage -import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.util.createIntent import com.android.settingslib.spa.gallery.R @@ -136,8 +135,8 @@ object PreferencePageProvider : SettingsPageProvider { Preference( object : PreferenceModel { override val title = ASYNC_PREFERENCE_TITLE - override val summary = model.asyncSummary - override val enabled = model.asyncEnable + override val summary = { model.asyncSummary.value } + override val enabled = { model.asyncEnable.value } } ) } @@ -170,7 +169,7 @@ object PreferencePageProvider : SettingsPageProvider { Preference( object : PreferenceModel { override val title = MANUAL_UPDATE_PREFERENCE_TITLE - override val summary = manualUpdaterSummary + override val summary = { manualUpdaterSummary.value } override val onClick = { model.manualUpdaterOnClick() } override val icon = @Composable { SettingsIcon(imageVector = Icons.Outlined.TouchApp) @@ -205,11 +204,13 @@ object PreferencePageProvider : SettingsPageProvider { createEntry(EntryEnum.AUTO_UPDATE_PREFERENCE) .setUiLayoutFn { val model = PreferencePageModel.create() - val autoUpdaterSummary = remember { model.getAutoUpdaterSummary() } + val autoUpdaterSummary = remember { + model.getAutoUpdaterSummary() + }.observeAsState(" ") Preference( object : PreferenceModel { override val title = AUTO_UPDATE_PREFERENCE_TITLE - override val summary = autoUpdaterSummary.observeAsState(" ") + override val summary = { autoUpdaterSummary.value } override val icon = @Composable { SettingsIcon(imageVector = Icons.Outlined.Autorenew) } @@ -250,12 +251,12 @@ object PreferencePageProvider : SettingsPageProvider { private fun singleLineSummaryEntry() = createEntry(EntryEnum.SINGLE_LINE_SUMMARY_PREFERENCE) .setUiLayoutFn { + val summary = stringResource(R.string.single_line_summary_preference_summary) Preference( model = object : PreferenceModel { override val title: String = stringResource(R.string.single_line_summary_preference_title) - override val summary = - stringResource(R.string.single_line_summary_preference_summary).toState() + override val summary = { summary } }, singleLineSummary = true, ) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt similarity index 96% rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt index b67e0664c4b753cc99e2ccc137e9ff3fb3fe1bda..ce0ee18e1bf74c08d70665cb7530d651f6ff3e3e 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePageProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -114,7 +114,7 @@ private fun SampleSwitchPreferenceWithSummary() { SwitchPreference(remember { object : SwitchPreferenceModel { override val title = "SwitchPreference" - override val summary = stateOf("With summary") + override val summary = { "With summary" } override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } } @@ -131,7 +131,7 @@ private fun SampleSwitchPreferenceWithAsyncSummary() { SwitchPreference(remember { object : SwitchPreferenceModel { override val title = "SwitchPreference" - override val summary = summary + override val summary = { summary.value } override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } } @@ -144,7 +144,7 @@ private fun SampleNotChangeableSwitchPreference() { SwitchPreference(remember { object : SwitchPreferenceModel { override val title = "SwitchPreference" - override val summary = stateOf("Not changeable") + override val summary = { "Not changeable" } override val changeable = stateOf(false) override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt similarity index 96% rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt index a2cd283ce346bcd2a6d05b3cfa4a77f3edbd463e..fc507451848444032effc38d1d323c1bc6fe3c29 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePageProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,7 +105,7 @@ private fun SampleTwoTargetSwitchPreferenceWithSummary() { TwoTargetSwitchPreference(remember { object : SwitchPreferenceModel { override val title = "TwoTargetSwitchPreference" - override val summary = stateOf("With summary") + override val summary = { "With summary" } override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } } @@ -122,7 +122,7 @@ private fun SampleTwoTargetSwitchPreferenceWithAsyncSummary() { TwoTargetSwitchPreference(remember { object : SwitchPreferenceModel { override val title = "TwoTargetSwitchPreference" - override val summary = summary + override val summary = { summary.value } override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } } @@ -135,7 +135,7 @@ private fun SampleNotChangeableTwoTargetSwitchPreference() { TwoTargetSwitchPreference(remember { object : SwitchPreferenceModel { override val title = "TwoTargetSwitchPreference" - override val summary = stateOf("Not changeable") + override val summary = { "Not changeable" } override val changeable = stateOf(false) override val checked = checked override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt similarity index 87% rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt index aeba6eabe82d15bdc9ae033fb9bd5a5b597a518f..5c5c504a43103aa4ba62fa640c93fde69436b08b 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPageProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,8 @@ package com.android.settingslib.spa.gallery.ui import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview @@ -58,7 +56,7 @@ object SpinnerPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { RegularScaffold(title = getTitle(arguments)) { - var selectedId by rememberSaveable { mutableStateOf(1) } + var selectedId by rememberSaveable { mutableIntStateOf(1) } Spinner( options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, selectedId = selectedId, @@ -66,9 +64,7 @@ object SpinnerPageProvider : SettingsPageProvider { ) Preference(object : PreferenceModel { override val title = "Selected id" - override val summary = remember { - derivedStateOf { selectedId.toString() } - } + override val summary = { selectedId.toString() } }) } } diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt index 814d4a1d1edee0f494bbb8e3d53ec64a09eaaaf2..fb65d65e5dc02a0f4ce417c3260b1c95848684cd 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/Bitmap.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.util import android.graphics.Bitmap import android.graphics.Canvas diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt deleted file mode 100644 index d7f42b3d1be3aa1b295e60464bd20ac1ec5ebb3d..0000000000000000000000000000000000000000 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/DefaultDeviceEmulationSpec.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.spa.screenshot - -import platform.test.screenshot.DeviceEmulationSpec -import platform.test.screenshot.DisplaySpec - -/** - * The emulations specs for all 8 permutations of: - * - phone or tablet. - * - dark of light mode. - * - portrait or landscape. - */ -val DeviceEmulationSpec.Companion.PhoneAndTabletFull - get() = PhoneAndTabletFullSpec - -private val PhoneAndTabletFullSpec = - DeviceEmulationSpec.forDisplays(Displays.Phone, Displays.Tablet) - -/** - * The emulations specs of: - * - phone + light mode + portrait. - * - phone + light mode + landscape. - * - tablet + dark mode + portrait. - * - * This allows to test the most important permutations of a screen/layout with only 3 - * configurations. - */ -val DeviceEmulationSpec.Companion.PhoneAndTabletMinimal - get() = PhoneAndTabletMinimalSpec - -private val PhoneAndTabletMinimalSpec = - DeviceEmulationSpec.forDisplays(Displays.Phone, isDarkTheme = false) + - DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = true, isLandscape = false) - -object Displays { - val Phone = - DisplaySpec( - "phone", - width = 1440, - height = 3120, - densityDpi = 560, - ) - - val Tablet = - DisplaySpec( - "tablet", - width = 2560, - height = 1600, - densityDpi = 320, - ) -} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt index 25bc098b29b76283b6fc05bf06e6ff206cd1f941..f5fba7fb3cc87bbaf29bbf739bea510e60842579 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsGoldenImagePathManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.util import androidx.test.platform.app.InstrumentationRegistry import platform.test.screenshot.GoldenImagePathManager diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt index 7a7cf318fb4f4a4f3b102246b1ff821a4849fae1..3dcefe9c783d9ee431a6ea1402d8b751205df18b 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.util import androidx.activity.ComponentActivity import androidx.compose.material3.MaterialTheme diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt index b2e0b181f88bf1f21e1a5bd283466b0f0036a80e..b74a2430c605d8b92d4e0ce33dec8ef49dfc36a7 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.button import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Launch import androidx.compose.material.icons.outlined.WarningAmber +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.button.ActionButton import com.android.settingslib.spa.widget.button.ActionButtons import org.junit.Rule @@ -27,6 +28,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -48,7 +50,7 @@ class ActionButtonsScreenshotTest(emulationSpec: DeviceEmulationSpec) { fun test() { screenshotRule.screenshotTest("actionButtons") { val actionButtons = listOf( - ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {}, + ActionButton(text = "Open", imageVector = Icons.AutoMirrored.Outlined.Launch) {}, ActionButton(text = "Uninstall", imageVector = Icons.Outlined.Delete) {}, ActionButton(text = "Force stop", imageVector = Icons.Outlined.WarningAmber) {}, ) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt index e6decb13c2b92cc4875aaa0dbfcf4a0807f363a0..051ef7733a699f77dc9ffca91a2c402232e35697 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.chart import androidx.compose.material3.MaterialTheme +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.chart.BarChart import com.android.settingslib.spa.widget.chart.BarChartData import com.android.settingslib.spa.widget.chart.BarChartModel @@ -26,6 +27,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -61,7 +63,7 @@ class BarChartScreenshotTest(emulationSpec: DeviceEmulationSpec) { override val colors = listOf(color) override val xValueFormatter = IAxisValueFormatter { value, _ -> - "${WeekDay.values()[value.toInt()]}" + "${WeekDay.entries[value.toInt()]}" } override val yValueFormatter = IAxisValueFormatter { value, _ -> diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt index f9d93f878f2b1c673f07bda696c769857850dab0..3822571758fc0f6954a751df20f8badff91c49c2 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.chart +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.chart.LineChart import com.android.settingslib.spa.widget.chart.LineChartData import com.android.settingslib.spa.widget.chart.LineChartModel @@ -26,6 +27,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -59,7 +61,7 @@ class LineChartScreenshotTest(emulationSpec: DeviceEmulationSpec) { ) override val xValueFormatter = IAxisValueFormatter { value, _ -> - "${WeekDay.values()[value.toInt()]}" + "${WeekDay.entries[value.toInt()]}" } override val yValueFormatter = IAxisValueFormatter { value, _ -> diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt index 34ded3cb94d6cc520e06f92dc343bc25c6f4ebff..6dd62ec032578aadf28a4c2b493c79c0ff5ae278 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.chart +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.chart.PieChart import com.android.settingslib.spa.widget.chart.PieChartData import com.android.settingslib.spa.widget.chart.PieChartModel @@ -24,6 +25,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -56,4 +58,4 @@ class PieChartScreenshotTest(emulationSpec: DeviceEmulationSpec) { ) } } -} \ No newline at end of file +} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt index 91aca05a9cdf1c0dc3645a2f9453d6b5c91017e1..0ccfc0b12fca2566d8d6255a213cc3a3567800d7 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.illustration +import com.android.settingslib.spa.screenshot.R +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.illustration.Illustration import com.android.settingslib.spa.widget.illustration.IllustrationModel import com.android.settingslib.spa.widget.illustration.ResourceType @@ -24,6 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt index a366b9e0e85bbdc7ca9bd517a9b1107cf56c9c64..c1d718891da465f3a5bfbf32407e89349aa88b85 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.MainSwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import org.junit.Rule @@ -25,6 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt index d72152c15af236e888341720b1af180ef764e4de..dd6b553f714d9c9a75ed95c82a17911302963565 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Autorenew import androidx.compose.material.icons.outlined.DisabledByDefault import androidx.compose.runtime.Composable -import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.ui.SettingsIcon @@ -30,6 +30,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -61,18 +62,18 @@ class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) { Preference(object : PreferenceModel { override val title = TITLE - override val summary = SUMMARY.toState() + override val summary = { SUMMARY } }) Preference(object : PreferenceModel { override val title = TITLE - override val summary = LONG_SUMMARY.toState() + override val summary = { LONG_SUMMARY } }) Preference(object : PreferenceModel { override val title = TITLE - override val summary = SUMMARY.toState() - override val enabled = false.toState() + override val summary = { SUMMARY } + override val enabled = { false } override val icon = @Composable { SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault) } @@ -80,7 +81,7 @@ class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) { Preference(object : PreferenceModel { override val title = TITLE - override val summary = SUMMARY.toState() + override val summary = { SUMMARY } override val icon = @Composable { SettingsIcon(imageVector = Icons.Outlined.Autorenew) } diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt index 5fcaf8503095940fa28dfa42f780db9927a56311..357d81593cb61bfcffbbed35ea6d20dc045b71e1 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.SystemUpdate import androidx.compose.runtime.Composable +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.ProgressBarPreference import com.android.settingslib.spa.widget.preference.ProgressBarPreferenceModel import com.android.settingslib.spa.widget.preference.ProgressBarWithDataPreference @@ -30,6 +31,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt index 48c922dcf1a12a23fd79919e5c3528fcf5243968..fdee7ee7048344f37730117df6b44b9a9a98e7f2 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AccessAlarm +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.SliderPreference import com.android.settingslib.spa.widget.preference.SliderPreferenceModel import org.junit.Rule @@ -26,6 +27,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt index 2c84a8e3f760cdb79506550a94f97754f45f9e83..a688e1196407474e9ff24d377f002c638ad80876 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AirplanemodeActive import androidx.compose.runtime.Composable import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.ui.SettingsIcon @@ -29,6 +30,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -72,7 +74,7 @@ private fun SampleSwitchPreference() { private fun SampleSwitchPreferenceWithSummary() { SwitchPreference(object : SwitchPreferenceModel { override val title = "SwitchPreference" - override val summary = stateOf("With summary") + override val summary = { "With summary" } override val checked = stateOf(true) override val onCheckedChange = null }) @@ -82,7 +84,7 @@ private fun SampleSwitchPreferenceWithSummary() { private fun SampleNotChangeableSwitchPreference() { SwitchPreference(object : SwitchPreferenceModel { override val title = "SwitchPreference" - override val summary = stateOf("Not changeable") + override val summary = { "Not changeable" } override val changeable = stateOf(false) override val checked = stateOf(true) override val onCheckedChange = null diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt index 2c37212f1a7179cfd93e7d197b08f2feee2cc630..8f0abc0cb2564fda24ef12abe25b52df57b4a707 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +14,19 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.preference import androidx.compose.foundation.layout.Column import com.android.settingslib.spa.framework.compose.stateOf -import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) @@ -54,7 +56,7 @@ class TwoTargetSwitchPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec TwoTargetSwitchPreference(object : SwitchPreferenceModel { override val title = "TwoTargetSwitchPreference" - override val summary = stateOf("With summary") + override val summary = { "With summary" } override val checked = stateOf(true) override val onCheckedChange = null }) {} diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt index 0a0faf6dd2c780dab1bcd2603e0e920e0b443007..fb01f7715def21b6bbf0f6951f87ca8c4bb68fa0 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,16 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.ui +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.ui.Footer import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt index 0b4d5e4ad6297e825e9c7b75143948feca5e8f34..2867741e2e0ae2371d86b39383ce3b806af31415 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.android.settingslib.spa.screenshot +package com.android.settingslib.spa.screenshot.widget.ui +import com.android.settingslib.spa.screenshot.util.SettingsScreenshotTestRule import com.android.settingslib.spa.widget.ui.Spinner import com.android.settingslib.spa.widget.ui.SpinnerOption import org.junit.Rule @@ -23,6 +24,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.PhoneAndTabletMinimal /** A screenshot test for ExampleFeature. */ @RunWith(Parameterized::class) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt index 078c925783fae5b676aca5c2a242ce5bfa3524ed..14af5084d62567f3ae94a49b278900bdb59c41ad 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,6 @@ import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.compose.localNavController import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.util.SESSION_BROWSE import com.android.settingslib.spa.framework.util.SESSION_SEARCH @@ -137,7 +136,7 @@ class DebugActivity : ComponentActivity() { val page = pageWithEntry.page Preference(object : PreferenceModel { override val title = "${page.debugBrief()} (${pageWithEntry.entries.size})" - override val summary = page.debugArguments().toState() + override val summary = { page.debugArguments() } override val onClick = navigator(route = ROUTE_PAGE + "/${page.id}") }) } @@ -179,8 +178,9 @@ class DebugActivity : ComponentActivity() { Text(text = "Entry size: ${pageWithEntry.entries.size}") Preference(model = object : PreferenceModel { override val title = "open page" - override val enabled = (spaEnvironment.browseActivityClass != null && - page.isBrowsable()).toState() + override val enabled = { + spaEnvironment.browseActivityClass != null && page.isBrowsable() + } override val onClick = openPage(page) }) EntryList(pageWithEntry.entries) @@ -196,9 +196,10 @@ class DebugActivity : ComponentActivity() { RegularScaffold(title = "Entry - ${entry.debugBrief()}") { Preference(model = object : PreferenceModel { override val title = "open entry" - override val enabled = (spaEnvironment.browseActivityClass != null && - entry.containerPage().isBrowsable()) - .toState() + override val enabled = { + spaEnvironment.browseActivityClass != null && + entry.containerPage().isBrowsable() + } override val onClick = openEntry(entry) }) Text(text = entryContent) @@ -210,8 +211,9 @@ class DebugActivity : ComponentActivity() { for (entry in entries) { Preference(object : PreferenceModel { override val title = entry.debugBrief() - override val summary = - "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}".toState() + override val summary = { + "${entry.fromPage?.displayName} -> ${entry.toPage?.displayName}" + } override val onClick = navigator(route = ROUTE_ENTRY + "/${entry.id}") }) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ListPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ListPreference.kt index 74f9c9d0f9e27fb4458881ec142ef36efe6eba2b..a0149da8f4b810af9960b70431a2861448265940 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ListPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ListPreference.kt @@ -27,8 +27,6 @@ import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.RadioButton import androidx.compose.runtime.Composable import androidx.compose.runtime.IntState -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,7 +35,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.dialog.SettingsDialog import com.android.settingslib.spa.widget.ui.SettingsDialogItem @@ -69,8 +66,8 @@ interface ListPreferenceModel { * * Disabled [ListPreference] will be displayed in disabled style. */ - val enabled: State<Boolean> - get() = stateOf(true) + val enabled: () -> Boolean + get() = { true } val options: List<ListPreferenceOption> @@ -89,7 +86,7 @@ fun ListPreference(model: ListPreferenceModel) { ) { Column(modifier = Modifier.selectableGroup()) { for (option in model.options) { - Radio(option, model.selectedId.intValue, model.enabled.value) { + Radio(option, model.selectedId.intValue, model.enabled()) { dialogOpened = false model.onIdSelected(it) } @@ -100,7 +97,7 @@ fun ListPreference(model: ListPreferenceModel) { Preference(model = remember(model) { object : PreferenceModel { override val title = model.title - override val summary = derivedStateOf { + override val summary = { model.options.find { it.id == model.selectedId.intValue }?.text ?: "" } override val icon = model.icon diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt index 7ecbec7ce0b434ffa1eabe45f7bc1e1c9527e727..bb7e8575ba1b042415759efa591624d9be278e75 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,12 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.foundation.clickable import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import com.android.settingslib.spa.framework.common.EntryMacro import com.android.settingslib.spa.framework.common.EntrySearchData import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.util.EntryHighlight import com.android.settingslib.spa.framework.util.wrapOnClickWithLog import com.android.settingslib.spa.widget.ui.createSettingsIcon @@ -42,9 +40,9 @@ data class SimplePreferenceMacro( override fun UiLayout() { Preference(model = object : PreferenceModel { override val title: String = this@SimplePreferenceMacro.title - override val summary = stateOf(this@SimplePreferenceMacro.summary ?: "") + override val summary = { this@SimplePreferenceMacro.summary ?: "" } override val icon = createSettingsIcon(this@SimplePreferenceMacro.icon) - override val enabled = stateOf(!this@SimplePreferenceMacro.disabled) + override val enabled = { !disabled } override val onClick = navigator(clickRoute) }) } @@ -69,8 +67,8 @@ interface PreferenceModel { /** * The summary of this [Preference]. */ - val summary: State<String> - get() = stateOf("") + val summary: () -> String + get() = { "" } /** * The icon of this [Preference]. @@ -85,8 +83,8 @@ interface PreferenceModel { * * Disabled [Preference] will be displayed in disabled style. */ - val enabled: State<Boolean> - get() = stateOf(true) + val enabled: () -> Boolean + get() = { true } /** * The on click handler of this [Preference]. @@ -108,10 +106,11 @@ fun Preference( singleLineSummary: Boolean = false, ) { val onClickWithLog = wrapOnClickWithLog(model.onClick) - val modifier = remember(model.enabled.value) { + val enabled = model.enabled() + val modifier = remember(enabled) { if (onClickWithLog != null) { Modifier.clickable( - enabled = model.enabled.value, + enabled = enabled, onClick = onClickWithLog ) } else Modifier @@ -119,11 +118,11 @@ fun Preference( EntryHighlight { BasePreference( title = model.title, - summary = { model.summary.value }, + summary = model.summary, singleLineSummary = singleLineSummary, modifier = modifier, icon = model.icon, - enabled = { model.enabled.value }, + enabled = model.enabled, ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt index f14f68ce74c0b0b7e8517589c41fa24f98227a91..12afe927b13b6388006707c4f122181a9b9fe256 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,8 +51,8 @@ interface SwitchPreferenceModel { /** * The summary of this [SwitchPreference]. */ - val summary: State<String> - get() = stateOf("") + val summary: () -> String + get() = { "" } /** * The icon of this [Preference]. @@ -95,7 +95,7 @@ fun SwitchPreference(model: SwitchPreferenceModel) { EntryHighlight { InternalSwitchPreference( title = model.title, - summary = { model.summary.value }, + summary = model.summary, icon = model.icon, checked = model.checked.value, changeable = model.changeable.value, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt index b8db63c9658e6327ce817b47725c53ae540f0961..986602394b3b3a984cfa8aed67ad1940f86cad07 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreference.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,32 +16,32 @@ package com.android.settingslib.spa.widget.preference -import com.android.settingslib.spa.framework.util.EntryHighlight +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.runtime.State import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.material3.Icon +import com.android.settingslib.spa.framework.util.EntryHighlight @Composable fun TwoTargetButtonPreference( - title: String, - summary: State<String>, - icon: @Composable (() -> Unit)? = null, - onClick: () -> Unit, - buttonIcon: ImageVector, - buttonIconDescription: String, - onButtonClick: () -> Unit + title: String, + summary: () -> String, + icon: @Composable (() -> Unit)? = null, + onClick: () -> Unit, + buttonIcon: ImageVector, + buttonIconDescription: String, + onButtonClick: () -> Unit ) { EntryHighlight { TwoTargetPreference( - title = title, - summary = summary, - onClick = onClick, - icon = icon) { + title = title, + summary = summary, + onClick = onClick, + icon = icon, + ) { IconButton(onClick = onButtonClick) { Icon(imageVector = buttonIcon, contentDescription = buttonIconDescription) } } } -} \ No newline at end of file +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt index 566361029ff6a0a0dc77168788d70a25043caa55..e36572fdff65abd666c37d877ea7a869a359b3d6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +34,7 @@ import com.android.settingslib.spa.framework.theme.divider @Composable internal fun TwoTargetPreference( title: String, - summary: State<String>, + summary: () -> String, onClick: () -> Unit, icon: @Composable (() -> Unit)? = null, widget: @Composable () -> Unit, diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ListPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ListPreferenceTest.kt index 997a02369d9f31d0a9407ba1138bca5ce167d191..796ac4851cb58dc0765354e6d4444454b60c16e7 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ListPreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ListPreferenceTest.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.testutils.onDialogText import org.junit.Rule import org.junit.Test @@ -92,7 +91,7 @@ class ListPreferenceTest { ListPreference(remember { object : ListPreferenceModel { override val title = TITLE - override val enabled = stateOf(false) + override val enabled = { false } override val options = listOf(ListPreferenceOption(id = 1, text = "A")) override val selectedId = mutableIntStateOf(1) override val onIdSelected: (Int) -> Unit = {} @@ -154,7 +153,7 @@ class ListPreferenceTest { ListPreference(remember { object : ListPreferenceModel { override val title = TITLE - override val enabled = enabledState + override val enabled = { enabledState.value } override val options = listOf( ListPreferenceOption(id = 1, text = "A"), ListPreferenceOption(id = 2, text = "B"), diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt index 06936e155b0665b1d8295d203cf42a0363b2e591..8c363db92e19dae6c266d0c8e848428f1d75851a 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -34,7 +33,6 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.toState import com.google.common.truth.Truth.assertThat import org.junit.Assert.fail import org.junit.Rule @@ -65,7 +63,7 @@ class PreferenceTest { Box(Modifier.width(BOX_WIDTH)) { Preference(object : PreferenceModel { override val title = TITLE - override val summary = LONG_SUMMARY.toState() + override val summary = { LONG_SUMMARY } }) } lineHeightDp = with(LocalDensity.current) { @@ -85,7 +83,7 @@ class PreferenceTest { Preference( model = object : PreferenceModel { override val title = TITLE - override val summary = LONG_SUMMARY.toState() + override val summary = { LONG_SUMMARY } }, singleLineSummary = true, ) @@ -113,7 +111,7 @@ class PreferenceTest { var count by remember { mutableStateOf(0) } Preference(object : PreferenceModel { override val title = TITLE - override val summary = derivedStateOf { count.toString() } + override val summary = { count.toString() } override val onClick: (() -> Unit) = { count++ } }) } @@ -128,8 +126,8 @@ class PreferenceTest { var count by remember { mutableStateOf(0) } Preference(object : PreferenceModel { override val title = TITLE - override val summary = derivedStateOf { count.toString() } - override val enabled = false.toState() + override val summary = { count.toString() } + override val enabled = { false } override val onClick: (() -> Unit) = { count++ } }) } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt index 2140c07090e1937c66f81363df29302008f4dbd3..e6d2401c55f726d10dabc241005eb141f171c835 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/ProgressBarPreferenceTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ package com.android.settingslib.spa.widget.preference import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Launch -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.material.icons.automirrored.outlined.Launch import androidx.compose.ui.semantics.ProgressBarRangeInfo import androidx.compose.ui.semantics.SemanticsProperties.ProgressBarRangeInfo import androidx.compose.ui.test.SemanticsMatcher @@ -49,11 +48,14 @@ class ProgressBarPreferenceTest { @Test fun data_displayed() { composeTestRule.setContent { - ProgressBarWithDataPreference(model = object : ProgressBarPreferenceModel { - override val title = "Title" - override val progress = 0.2f - override val icon: ImageVector = Icons.Outlined.Launch - }, data = "Data") + ProgressBarWithDataPreference( + model = object : ProgressBarPreferenceModel { + override val title = "Title" + override val progress = 0.2f + override val icon = Icons.AutoMirrored.Outlined.Launch + }, + data = "Data", + ) } composeTestRule.onNodeWithText("Title").assertIsDisplayed() composeTestRule.onNodeWithText("Data").assertIsDisplayed() diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt index 3a2b445e21487ca8ac90f3ac37daefee458e4c26..6de1933410049bc6949dd639abd708f2bba43267 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetButtonPreferenceTest.kt @@ -1,3 +1,19 @@ +/* + * 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.settingslib.spa.widget.preference import androidx.compose.material.icons.Icons @@ -9,7 +25,6 @@ import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.toState import com.google.common.truth.Truth import org.junit.Rule import org.junit.Test @@ -64,10 +79,10 @@ private fun testTwoTargetButtonPreference( ) { TwoTargetButtonPreference( title = TEST_MODEL_TITLE, - summary = TEST_MODEL_SUMMARY.toState(), + summary = { TEST_MODEL_SUMMARY }, onClick = onClick, buttonIcon = TEST_BUTTON_ICON, buttonIconDescription = TEST_BUTTON_ICON_DESCRIPTION, onButtonClick = onButtonClick ) -} \ No newline at end of file +} diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt index 16e09ee854d80b6450d9146d4ea14a5123a53d77..09a6e6ddc7f0718e05f1bbb712c6104c7dc06a37 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CategoryTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import org.junit.Rule @@ -50,7 +49,7 @@ class CategoryTest { Preference(remember { object : PreferenceModel { override val title = "Some Preference" - override val summary = stateOf("Some summary") + override val summary = { "Some summary" } } }) } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt index f3ab80c7b8c206e6c3a87ebb9ea01723b23fd27f..9eee6adb5d1753db58d49c5cf9ad1147470d60f8 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt @@ -19,7 +19,6 @@ package com.android.settingslib.spaprivileged.model.app import android.content.pm.ApplicationInfo import android.icu.text.CollationKey import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import com.android.settingslib.spa.widget.ui.SpinnerOption import com.android.settingslib.spaprivileged.template.app.AppListItem import com.android.settingslib.spaprivileged.template.app.AppListItemModel @@ -89,7 +88,7 @@ interface AppListModel<T : AppRecord> { * @return null if no summary should be displayed. */ @Composable - fun getSummary(option: Int, record: T): State<String>? = null + fun getSummary(option: Int, record: T): (() -> String)? = null @Composable fun AppListItemModel<T>.AppItem() { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 066db3475b364fd5aaa5fafc3cad3db270275fe9..7c45b64233b2942babdcd1777ccc0af8c6be4102 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -36,7 +36,6 @@ import com.android.settingslib.spa.framework.compose.LifecycleEffect import com.android.settingslib.spa.framework.compose.LogCompositions import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll -import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.widget.ui.CategoryTitle import com.android.settingslib.spa.widget.ui.PlaceholderTitle import com.android.settingslib.spa.widget.ui.Spinner @@ -150,7 +149,7 @@ private fun <T : AppRecord> AppListModel<T>.AppListWidget( ?.let { group -> CategoryTitle(title = group) } val appEntry = list[it] - val summary = getSummary(option, appEntry.record) ?: "".toState() + val summary = getSummary(option, appEntry.record) ?: { "" } remember(appEntry) { AppListItemModel(appEntry.record, appEntry.label, summary) }.AppItem() diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItemModel.kt similarity index 89% rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItemModel.kt index 6d0d7d62bd5f75a4a4127e7f42ccc819ed8548cc..a7c5036200d1d2d731406cfda93348a2bb841609 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItem.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListItemModel.kt @@ -17,11 +17,9 @@ package com.android.settingslib.spaprivileged.template.app import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -import com.android.settingslib.spa.framework.compose.toState import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference @@ -31,7 +29,7 @@ import com.android.settingslib.spaprivileged.model.app.AppRecord data class AppListItemModel<T : AppRecord>( val record: T, val label: String, - val summary: State<String>, + val summary: () -> String, ) @Composable @@ -55,6 +53,6 @@ private fun AppListItemPreview() { val record = object : AppRecord { override val app = LocalContext.current.applicationInfo } - AppListItemModel<AppRecord>(record, "Chrome", "Allowed".toState()).AppListItem {} + AppListItemModel<AppRecord>(record, "Chrome", { "Allowed" }).AppListItem {} } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt index 17e970845f5892331ded9601b61467735ee1e64c..3ab27367fd167fcd10f11de9d3d5adbf77ff9eee 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt @@ -20,8 +20,7 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -149,33 +148,27 @@ internal class TogglePermissionInternalAppListModel<T : AppRecord>( override fun getSummary(option: Int, record: T) = getSummary(record) @Composable - fun getSummary(record: T): State<String> { + fun getSummary(record: T): () -> String { val restrictions = remember(record.app.userId) { Restrictions( userId = record.app.userId, keys = listModel.switchRestrictionKeys, ) } - val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions) - val allowed = listModel.isAllowed(record) - return remember { - derivedStateOf { - RestrictedSwitchPreference.getSummary( - context = context, - restrictedMode = restrictedMode.value, - summaryIfNoRestricted = getSummaryIfNoRestricted(allowed), - checked = allowed, - ).value - } - } + val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions) + val allowed by listModel.isAllowed(record) + return RestrictedSwitchPreference.getSummary( + context = context, + restrictedModeSupplier = { restrictedMode }, + summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed) }, + checked = { allowed }, + ) } - private fun getSummaryIfNoRestricted(allowed: State<Boolean?>) = derivedStateOf { - when (allowed.value) { - true -> context.getString(R.string.app_permission_summary_allowed) - false -> context.getString(R.string.app_permission_summary_not_allowed) - null -> context.getPlaceholder() - } + private fun getSummaryIfNoRestricted(allowed: Boolean?): String = when (allowed) { + true -> context.getString(R.string.app_permission_summary_allowed) + false -> context.getString(R.string.app_permission_summary_not_allowed) + null -> context.getPlaceholder() } @Composable diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt index 50490c0b887de507ebcc779b5c8217d854bfd5ff..ac85dd4249f69b622e4e4669e6e1c5839bd74b90 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedPreference.kt @@ -23,7 +23,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted @@ -73,7 +72,7 @@ private class RestrictedPreferenceModel( override val enabled = when (restrictedMode) { NoRestricted -> model.enabled - else -> stateOf(false) + else -> ({ false }) } override val onClick = when (restrictedMode) { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt index 2129403c2d85176588bca4ecbb3fdad5a308811f..d17e0c792c5b3419f244d6c5fb6fdf3bc87963be 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt @@ -21,8 +21,6 @@ import androidx.annotation.VisibleForTesting import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -75,17 +73,16 @@ internal fun RestrictedSwitchPreference( internal object RestrictedSwitchPreference { fun getSummary( context: Context, - restrictedMode: RestrictedMode?, - summaryIfNoRestricted: State<String>, - checked: State<Boolean?>, - ): State<String> = when (restrictedMode) { - is NoRestricted -> summaryIfNoRestricted - is BaseUserRestricted -> stateOf( - context.getString(com.android.settingslib.R.string.disabled) - ) - - is BlockedByAdmin -> derivedStateOf { restrictedMode.getSummary(checked.value) } - null -> stateOf(context.getPlaceholder()) + restrictedModeSupplier: () -> RestrictedMode?, + summaryIfNoRestricted: () -> String, + checked: () -> Boolean?, + ): () -> String = { + when (val restrictedMode = restrictedModeSupplier()) { + is NoRestricted -> summaryIfNoRestricted() + is BaseUserRestricted -> context.getString(com.android.settingslib.R.string.disabled) + is BlockedByAdmin -> restrictedMode.getSummary(checked()) + null -> context.getPlaceholder() + } } } @@ -98,9 +95,9 @@ private class RestrictedSwitchPreferenceModel( override val summary = RestrictedSwitchPreference.getSummary( context = context, - restrictedMode = restrictedMode, + restrictedModeSupplier = { restrictedMode }, summaryIfNoRestricted = model.summary, - checked = model.checked, + checked = { model.checked.value }, ) override val checked = when (restrictedMode) { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt index 2fd1b10dade803acbf082140ad3d57794d830570..c29d7c2ca363f090c1f20212172d836f343cbd13 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListSwitchItemTest.kt @@ -158,7 +158,7 @@ class AppListSwitchItemTest { override val app = APP }, label = LABEL, - summary = stateOf(SUMMARY), + summary = { SUMMARY }, ) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt index 6e7fc8e4416cce6a30b42fa5637e40cd9a3bb63c..644a2d7daf9b288478f56635e1fe9c997ddf0a4a 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTwoTargetSwitchItemTest.kt @@ -183,7 +183,7 @@ class AppListTwoTargetSwitchItemTest { override val app = APP }, label = LABEL, - summary = stateOf(SUMMARY), + summary = { SUMMARY }, ) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt index 457b810f2dec38e5690cac6be26092ac8722b1c2..bf0ad0b290721894898c3f26baf7824ce5d199d7 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt @@ -19,7 +19,6 @@ package com.android.settingslib.spaprivileged.template.app import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.State import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule @@ -27,7 +26,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.testutils.FakeNavControllerWrapper import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder @@ -72,10 +70,9 @@ class TogglePermissionAppListPageTest { fakeRestrictionsProvider.restrictedMode = NoRestricted val listModel = TestTogglePermissionAppListModel(isAllowed = true) - val summaryState = getSummary(listModel) + val summary = getSummary(listModel) - assertThat(summaryState.value) - .isEqualTo(context.getString(R.string.app_permission_summary_allowed)) + assertThat(summary).isEqualTo(context.getString(R.string.app_permission_summary_allowed)) } @Test @@ -83,9 +80,9 @@ class TogglePermissionAppListPageTest { fakeRestrictionsProvider.restrictedMode = NoRestricted val listModel = TestTogglePermissionAppListModel(isAllowed = false) - val summaryState = getSummary(listModel) + val summary = getSummary(listModel) - assertThat(summaryState.value) + assertThat(summary) .isEqualTo(context.getString(R.string.app_permission_summary_not_allowed)) } @@ -94,9 +91,9 @@ class TogglePermissionAppListPageTest { fakeRestrictionsProvider.restrictedMode = NoRestricted val listModel = TestTogglePermissionAppListModel(isAllowed = null) - val summaryState = getSummary(listModel) + val summary = getSummary(listModel) - assertThat(summaryState.value).isEqualTo(context.getPlaceholder()) + assertThat(summary).isEqualTo(context.getPlaceholder()) } @Test @@ -108,7 +105,7 @@ class TogglePermissionAppListPageTest { AppListItemModel( record = listModel.transformItem(APP), label = LABEL, - summary = stateOf(SUMMARY), + summary = { SUMMARY }, ).AppItem() } } @@ -152,12 +149,12 @@ class TogglePermissionAppListPageTest { restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider }, ) - private fun getSummary(listModel: TestTogglePermissionAppListModel): State<String> { - lateinit var summary: State<String> + private fun getSummary(listModel: TestTogglePermissionAppListModel): String { + lateinit var summary: () -> String composeTestRule.setContent { summary = createInternalAppListModel(listModel).getSummary(record = TestAppRecord(APP)) } - return summary + return summary() } private companion object { diff --git a/packages/SettingsLib/res/drawable/ic_hdmi.xml b/packages/SettingsLib/res/drawable/ic_hdmi.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7a553bddb8c722c004bfdf392895e663c4a7c51 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_hdmi.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M320,880L320,760L200,521L200,280L240,280L240,140Q240,117 258.5,98.5Q277,80 300,80L660,80Q683,80 701.5,98.5Q720,117 720,140L720,280L760,280L760,521L640,760L640,880L320,880ZM300,280L398,280L398,198L432,198L432,280L528,280L528,198L562,198L562,280L660,280L660,140Q660,140 660,140Q660,140 660,140L300,140Q300,140 300,140Q300,140 300,140L300,280ZM378,743L378,743L582,743L582,743L582,743L378,743L378,743ZM378,743L582,743L700,504L700,340L260,340L260,504L378,743Z"/> +</vector> diff --git a/packages/SettingsLib/res/drawable/ic_tv.xml b/packages/SettingsLib/res/drawable/ic_tv.xml new file mode 100644 index 0000000000000000000000000000000000000000..87abaf42bd65193c9d5906ef9fd697e74fc2f737 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_tv.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M320,840L320,760L160,760Q127,760 103.5,736.5Q80,713 80,680L80,200Q80,167 103.5,143.5Q127,120 160,120L800,120Q833,120 856.5,143.5Q880,167 880,200L880,680Q880,713 856.5,736.5Q833,760 800,760L640,760L640,840L320,840ZM160,680L800,680Q800,680 800,680Q800,680 800,680L800,200Q800,200 800,200Q800,200 800,200L160,200Q160,200 160,200Q160,200 160,200L160,680Q160,680 160,680Q160,680 160,680ZM160,680Q160,680 160,680Q160,680 160,680L160,200Q160,200 160,200Q160,200 160,200L160,200Q160,200 160,200Q160,200 160,200L160,680Q160,680 160,680Q160,680 160,680L160,680Z"/> +</vector> \ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/ic_usb.xml b/packages/SettingsLib/res/drawable/ic_usb.xml new file mode 100644 index 0000000000000000000000000000000000000000..b5f15eae3855de72282dc68b1d26fa08232b74d0 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_usb.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,880Q448,880 428,860Q408,840 408,808Q408,786 419,768Q430,750 450,739L450,628L302,628Q278,628 260,610Q242,592 242,568L242,459Q222,450 211,432.39Q200,414.78 200,392.28Q200,360 220,340Q240,320 272,320Q304,320 324,340Q344,360 344,392.41Q344,415 333,432.5Q322,450 302,459L302,568Q302,568 302,568Q302,568 302,568L450,568L450,228L370,228L480,79L590,228L510,228L510,568L658,568Q658,568 658,568Q658,568 658,568L658,464L616,464L616,320L760,320L760,464L718,464L718,568Q718,592 700,610Q682,628 658,628L510,628L510,739Q529.95,749.65 540.97,768.83Q552,788 552,808Q552,840 532,860Q512,880 480,880Z"/> +</vector> \ No newline at end of file diff --git a/packages/SettingsLib/res/drawable/ic_wired_device.xml b/packages/SettingsLib/res/drawable/ic_wired_device.xml new file mode 100644 index 0000000000000000000000000000000000000000..7964c9f08d7e61cd9ec91f7fac103919a193de41 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_wired_device.xml @@ -0,0 +1,25 @@ +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M123,920L123,750Q89,737 64.5,707.5Q40,678 40,643L40,245L123,245L123,70Q123,56 131,48Q139,40 153,40Q167,40 175,48Q183,56 183,70L183,245L266,245L266,643Q266,678 242,707.5Q218,737 183,750L183,920L123,920ZM450,920L450,750Q416,737 391.5,707.5Q367,678 367,643L367,245L450,245L450,70Q450,56 458,48Q466,40 480,40Q494,40 502,48Q510,56 510,70L510,245L593,245L593,643Q593,678 569,707.5Q545,737 510,750L510,920L450,920ZM777,920L777,750Q743,737 718.5,707.5Q694,678 694,643L694,245L777,245L777,70Q777,56 785,48Q793,40 807,40Q821,40 829,48Q837,56 837,70L837,245L920,245L920,643Q920,678 895.5,707.5Q871,737 837,750L837,920L777,920ZM100,489L206,489L206,305L100,305L100,489ZM427,489L533,489L533,305L427,305L427,489ZM754,489L860,489L860,305L754,305L754,489ZM153,489L153,489L153,489L153,489L153,489ZM480,489L480,489L480,489L480,489L480,489ZM807,489L807,489L807,489L807,489L807,489ZM100,489L100,489L206,489L206,489L100,489ZM427,489L427,489L533,489L533,489L427,489ZM754,489L754,489L860,489L860,489L754,489Z"/> +</vector> \ No newline at end of file diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 96029c813528f72b2aaddfb96dd78584f1c0e85d..1a5acf650cb4db9c10be700d416c6a1d71b11aab 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1334,6 +1334,8 @@ <string name="media_transfer_this_device_name" product="default">This phone</string> <!-- Name of the tablet device. [CHAR LIMIT=30] --> <string name="media_transfer_this_device_name" product="tablet">This tablet</string> + <!-- Name of the default media output of the TV. [CHAR LIMIT=30] --> + <string name="media_transfer_this_device_name" product="tv">@string/tv_media_transfer_default</string> <!-- Name of the dock device. [CHAR LIMIT=30] --> <string name="media_transfer_dock_speaker_device_name">Dock speaker</string> <!-- Default name of the external device. [CHAR LIMIT=30] --> @@ -1357,6 +1359,26 @@ <!-- Sub status indicates the device does not support the current media track. [CHAR LIMIT=NONE] --> <string name="media_output_status_track_unsupported">Can\’t play this media here</string> + <!-- Media output switcher. Default subtitle for any output option that is connected if no more information is known [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_connected">Connected</string> + + <!-- TV media output switcher. Title for devices connected through HDMI ARC if no device name is available. [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_arc_fallback_title">HDMI ARC</string> + <!-- TV media output switcher. Title for devices connected through HDMI EARC if no device name is available. [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_earc_fallback_title">HDMI eARC</string> + + <!-- TV media output switcher. Subtitle for devices connected through HDMI ARC if a device name is available. [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_arc_subtitle">Connected via ARC</string> + <!-- Media output switcher. Subtitle for devices connected through HDMI EARC if a device name is available. [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_earc_subtitle">Connected via eARC</string> + + <!-- TV media output switcher. Title for the default audio output of the device [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_default">TV Default</string> + <!-- TV media output switcher. Subtitle for default audio output which is HDMI, e.g. TV dongle [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_hdmi">HDMI Output</string> + <!-- TV media output switcher. Subtitle for default audio output which is internal speaker, i.e. panel VTs [CHAR LIMIT=NONE] --> + <string name="tv_media_transfer_internal_speakers">Internal Speakers</string> + <!-- Warning message to tell user is have problem during profile connect, it need to turn off device and back on. [CHAR_LIMIT=NONE] --> <string name="profile_connect_timeout_subtext">Problem connecting. Turn device off & back on</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java index 2a2841745fa06d4cd01cd66b2dae1ebc0c028bc0..cf224dc3be5f3b93224a7a90924b4b0e0892f4e5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java @@ -18,11 +18,13 @@ package com.android.settingslib.media; import android.annotation.DrawableRes; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.media.AudioDeviceInfo; import android.media.MediaRoute2Info; import com.android.settingslib.R; +import com.android.settingslib.media.flags.Flags; import java.util.Arrays; import java.util.HashMap; @@ -31,16 +33,25 @@ import java.util.Map; /** A util class to get the appropriate icon for different device types. */ public class DeviceIconUtil { + + // A default icon to use if the type is not present in the map. + @DrawableRes private static final int DEFAULT_ICON = R.drawable.ic_smartphone; + @DrawableRes private static final int DEFAULT_ICON_TV = R.drawable.ic_media_speaker_device; + // A map from a @AudioDeviceInfo.AudioDeviceType to full device information. private final Map<Integer, Device> mAudioDeviceTypeToIconMap = new HashMap<>(); // A map from a @MediaRoute2Info.Type to full device information. private final Map<Integer, Device> mMediaRouteTypeToIconMap = new HashMap<>(); - // A default icon to use if the type is not present in the map. - @DrawableRes private static final int DEFAULT_ICON = R.drawable.ic_smartphone; - public DeviceIconUtil() { - List<Device> deviceList = - Arrays.asList( + private final boolean mIsTv; + + public DeviceIconUtil(Context context) { + this(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)); + } + + public DeviceIconUtil(boolean isTv) { + mIsTv = isTv && Flags.enableTvMediaOutputDialog(); + List<Device> deviceList = Arrays.asList( new Device( AudioDeviceInfo.TYPE_USB_DEVICE, MediaRoute2Info.TYPE_USB_DEVICE, @@ -52,7 +63,7 @@ public class DeviceIconUtil { new Device( AudioDeviceInfo.TYPE_USB_ACCESSORY, MediaRoute2Info.TYPE_USB_ACCESSORY, - R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_usb : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_DOCK, MediaRoute2Info.TYPE_DOCK, @@ -60,29 +71,27 @@ public class DeviceIconUtil { new Device( AudioDeviceInfo.TYPE_HDMI, MediaRoute2Info.TYPE_HDMI, - R.drawable.ic_headphone), - // TODO: b/306359110 - Put proper iconography for HDMI_ARC type. + mIsTv ? R.drawable.ic_tv : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_HDMI_ARC, MediaRoute2Info.TYPE_HDMI_ARC, - R.drawable.ic_headphone), - // TODO: b/306359110 - Put proper iconography for HDMI_EARC type. + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_HDMI_EARC, MediaRoute2Info.TYPE_HDMI_EARC, - R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_WIRED_HEADSET, MediaRoute2Info.TYPE_WIRED_HEADSET, - R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_wired_device : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_WIRED_HEADPHONES, MediaRoute2Info.TYPE_WIRED_HEADPHONES, - R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_wired_device : R.drawable.ic_headphone), new Device( AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, MediaRoute2Info.TYPE_BUILTIN_SPEAKER, - R.drawable.ic_smartphone)); + mIsTv ? R.drawable.ic_tv : R.drawable.ic_smartphone)); for (int i = 0; i < deviceList.size(); i++) { Device device = deviceList.get(i); mAudioDeviceTypeToIconMap.put(device.mAudioDeviceType, device); @@ -90,6 +99,10 @@ public class DeviceIconUtil { } } + private int getDefaultIcon() { + return mIsTv ? DEFAULT_ICON_TV : DEFAULT_ICON; + } + /** Returns a drawable for an icon representing the given audioDeviceType. */ public Drawable getIconFromAudioDeviceType( @AudioDeviceInfo.AudioDeviceType int audioDeviceType, Context context) { @@ -103,7 +116,7 @@ public class DeviceIconUtil { if (mAudioDeviceTypeToIconMap.containsKey(audioDeviceType)) { return mAudioDeviceTypeToIconMap.get(audioDeviceType).mIconDrawableRes; } - return DEFAULT_ICON; + return getDefaultIcon(); } /** Returns a drawable res ID for an icon representing the given mediaRouteType. */ @@ -113,7 +126,7 @@ public class DeviceIconUtil { if (mMediaRouteTypeToIconMap.containsKey(mediaRouteType)) { return mMediaRouteTypeToIconMap.get(mediaRouteType).mIconDrawableRes; } - return DEFAULT_ICON; + return getDefaultIcon(); } private static class Device { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index c44f66e99d00148660755b65d421cabeb8c05e08..80eeab56cc45c711eef28e655d2c9982ef135696 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -28,15 +28,24 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER; +import android.Manifest; import android.annotation.NonNull; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.hardware.hdmi.HdmiControlManager; +import android.hardware.hdmi.HdmiDeviceInfo; +import android.hardware.hdmi.HdmiPortInfo; import android.media.MediaRoute2Info; import android.media.RouteListingPreference; +import android.util.Log; import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; +import com.android.settingslib.media.flags.Flags; + +import java.util.List; /** * PhoneMediaDevice extends MediaDevice to represents Phone device. @@ -58,6 +67,7 @@ public class PhoneMediaDevice extends MediaDevice { public static String getSystemRouteNameFromType( @NonNull Context context, @NonNull MediaRoute2Info routeInfo) { CharSequence name; + boolean isTv = isTv(context); switch (routeInfo.getType()) { case TYPE_WIRED_HEADSET: case TYPE_WIRED_HEADPHONES: @@ -73,9 +83,32 @@ public class PhoneMediaDevice extends MediaDevice { name = context.getString(R.string.media_transfer_this_device_name); break; case TYPE_HDMI: + name = context.getString(isTv ? R.string.tv_media_transfer_default : + R.string.media_transfer_external_device_name); + break; case TYPE_HDMI_ARC: + if (isTv) { + String deviceName = getHdmiOutDeviceName(context); + if (deviceName != null) { + name = deviceName; + } else { + name = context.getString(R.string.tv_media_transfer_arc_fallback_title); + } + } else { + name = context.getString(R.string.media_transfer_external_device_name); + } + break; case TYPE_HDMI_EARC: - name = context.getString(R.string.media_transfer_external_device_name); + if (isTv) { + String deviceName = getHdmiOutDeviceName(context); + if (deviceName != null) { + name = deviceName; + } else { + name = context.getString(R.string.tv_media_transfer_arc_fallback_title); + } + } else { + name = context.getString(R.string.media_transfer_external_device_name); + } break; default: name = context.getString(R.string.media_transfer_default_device_name); @@ -94,10 +127,15 @@ public class PhoneMediaDevice extends MediaDevice { String packageName, RouteListingPreference.Item item) { super(context, info, packageName, item); - mDeviceIconUtil = new DeviceIconUtil(); + mDeviceIconUtil = new DeviceIconUtil(mContext); initDeviceRecord(); } + static boolean isTv(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) + && Flags.enableTvMediaOutputDialog(); + } + // MediaRoute2Info.getType was made public on API 34, but exists since API 30. @SuppressWarnings("NewApi") @Override @@ -111,9 +149,64 @@ public class PhoneMediaDevice extends MediaDevice { return SELECTION_BEHAVIOR_TRANSFER; } + private static String getHdmiOutDeviceName(Context context) { + HdmiControlManager hdmiControlManager; + if (context.checkCallingOrSelfPermission(Manifest.permission.HDMI_CEC) + == PackageManager.PERMISSION_GRANTED) { + hdmiControlManager = context.getSystemService(HdmiControlManager.class); + } else { + Log.w(TAG, "Could not get HDMI device name, android.permission.HDMI_CEC denied"); + return null; + } + + HdmiPortInfo hdmiOutputPortInfo = null; + for (HdmiPortInfo hdmiPortInfo : hdmiControlManager.getPortInfo()) { + if (hdmiPortInfo.getType() == HdmiPortInfo.PORT_OUTPUT) { + hdmiOutputPortInfo = hdmiPortInfo; + break; + } + } + if (hdmiOutputPortInfo == null) { + return null; + } + List<HdmiDeviceInfo> connectedDevices = hdmiControlManager.getConnectedDevices(); + for (HdmiDeviceInfo deviceInfo : connectedDevices) { + if (deviceInfo.getPortId() == hdmiOutputPortInfo.getId()) { + String deviceName = deviceInfo.getDisplayName(); + if (deviceName != null && !deviceName.isEmpty()) { + return deviceName; + } + } + } + return null; + } + @Override public String getSummary() { - return mSummary; + if (!isTv(mContext)) { + return mSummary; + } + switch (mRouteInfo.getType()) { + case TYPE_BUILTIN_SPEAKER: + return mContext.getString(R.string.tv_media_transfer_internal_speakers); + case TYPE_HDMI: + return mContext.getString(R.string.tv_media_transfer_hdmi); + case TYPE_HDMI_ARC: + if (getHdmiOutDeviceName(mContext) == null) { + // Connection type is already part of the title. + return mContext.getString(R.string.tv_media_transfer_connected); + } + return mContext.getString(R.string.tv_media_transfer_arc_subtitle); + case TYPE_HDMI_EARC: + if (getHdmiOutDeviceName(mContext) == null) { + // Connection type is already part of the title. + return mContext.getString(R.string.tv_media_transfer_connected); + } + return mContext.getString(R.string.tv_media_transfer_earc_subtitle); + default: + return null; + } + } @Override diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java index 72dfc1733275000946beb1bbde6a6471d4e727cf..5669276a0424e2c9364e3a001f937eb20a17e03a 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/DeviceIconUtilTest.java @@ -20,131 +20,309 @@ import static com.google.common.truth.Truth.assertThat; import android.media.AudioDeviceInfo; import android.media.MediaRoute2Info; +import android.platform.test.flag.junit.SetFlagsRule; import com.android.settingslib.R; +import com.android.settingslib.media.flags.Flags; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class DeviceIconUtilTest { - private final DeviceIconUtil mDeviceIconUtil = new DeviceIconUtil(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TV_MEDIA_OUTPUT_DIALOG); + } @Test public void getIconResIdFromMediaRouteType_usbDevice_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_DEVICE)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); } @Test public void getIconResIdFromMediaRouteType_usbHeadset_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_HEADSET)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); } @Test public void getIconResIdFromMediaRouteType_usbAccessory_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_ACCESSORY)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_usbAccessory_isUsb() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_usb); + } + + @Test + public void getIconResIdFromMediaRouteType_dock_isDock() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_dock_device); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_dock_device); + } + + @Test + public void getIconResIdFromMediaRouteType_hdmi() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_hdmi_isTv() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_tv); + } + + @Test + public void getIconResIdFromMediaRouteType_hdmiArc_isHeadphone() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI_ARC)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_hdmiArc_isHdmi() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI_ARC)) + .isEqualTo(R.drawable.ic_hdmi); } @Test - public void getIconResIdFromMediaRouteType_dock_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_DOCK)) - .isEqualTo(R.drawable.ic_headphone); + public void getIconResIdFromMediaRouteType_hdmiEarc_isHeadphone() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI_EARC)) + .isEqualTo(R.drawable.ic_headphone); } @Test - public void getIconResIdFromMediaRouteType_hdmi_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI)) - .isEqualTo(R.drawable.ic_headphone); + public void getIconResIdFromMediaRouteType_tv_hdmiEarc_isHdmi() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_HDMI_EARC)) + .isEqualTo(R.drawable.ic_hdmi); } @Test public void getIconResIdFromMediaRouteType_wiredHeadset_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADSET)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_wiredHeadset_isWiredDevice() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_wired_device); } @Test public void getIconResIdFromMediaRouteType_wiredHeadphones_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADPHONES)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_wiredHeadphones_isWiredDevice() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_wired_device); } @Test public void getIconResIdFromMediaRouteType_builtinSpeaker_isSmartphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER)) - .isEqualTo(R.drawable.ic_smartphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_builtinSpeaker_isTv() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_tv); } @Test public void getIconResIdFromMediaRouteType_unsupportedType_isSmartphone() { - assertThat(mDeviceIconUtil.getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN)) - .isEqualTo(R.drawable.ic_smartphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromMediaRouteType_tv_unsupportedType_isSpeaker() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromMediaRouteType(MediaRoute2Info.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_media_speaker_device); } @Test public void getIconResIdFromAudioDeviceType_usbDevice_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_DEVICE)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_DEVICE)) + .isEqualTo(R.drawable.ic_headphone); } @Test public void getIconResIdFromAudioDeviceType_usbHeadset_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_HEADSET)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); } @Test public void getIconResIdFromAudioDeviceType_usbAccessory_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_ACCESSORY)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_usbAccessory_isUsb() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_USB_ACCESSORY)) + .isEqualTo(R.drawable.ic_usb); } @Test - public void getIconResIdFromAudioDeviceType_dock_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_DOCK)) - .isEqualTo(R.drawable.ic_headphone); + public void getIconResIdFromAudioDeviceType_dock_isDock() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_dock_device); + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_DOCK)) + .isEqualTo(R.drawable.ic_dock_device); } @Test public void getIconResIdFromAudioDeviceType_hdmi_isHeadphone() { - assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_hdmi_isTv() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI)) + .isEqualTo(R.drawable.ic_tv); + } + + @Test + public void getIconResIdFromAudioDeviceType_hdmiArc_isHeadphone() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI_ARC)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_hdmiArc_isHdmi() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI_ARC)) + .isEqualTo(R.drawable.ic_hdmi); + } + + @Test + public void getIconResIdFromAudioDeviceType_hdmiEarc_isHeadphone() { + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI_EARC)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_hdmiEarc() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_HDMI_EARC)) + .isEqualTo(R.drawable.ic_hdmi); } @Test public void getIconResIdFromAudioDeviceType_wiredHeadset_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADSET)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_wiredHeadset_isWiredDevice() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADSET)) + .isEqualTo(R.drawable.ic_wired_device); } @Test public void getIconResIdFromAudioDeviceType_wiredHeadphones_isHeadphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADPHONES)) - .isEqualTo(R.drawable.ic_headphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_headphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_wiredHeadphones_isWiredDevice() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_WIRED_HEADPHONES)) + .isEqualTo(R.drawable.ic_wired_device); } @Test public void getIconResIdFromAudioDeviceType_builtinSpeaker_isSmartphone() { - assertThat( - mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) - .isEqualTo(R.drawable.ic_smartphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_builtinSpeaker_isTv() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)) + .isEqualTo(R.drawable.ic_tv); } @Test public void getIconResIdFromAudioDeviceType_unsupportedType_isSmartphone() { - assertThat(mDeviceIconUtil.getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_UNKNOWN)) - .isEqualTo(R.drawable.ic_smartphone); + assertThat(new DeviceIconUtil(/* isTv */ false) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_smartphone); + } + + @Test + public void getIconResIdFromAudioDeviceType_tv_unsupportedType_isSpeaker() { + assertThat(new DeviceIconUtil(/* isTv */ true) + .getIconResIdFromAudioDeviceType(AudioDeviceInfo.TYPE_UNKNOWN)) + .isEqualTo(R.drawable.ic_media_speaker_device); } } diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 4c9436b4e151adf477b668e18b39a8586de5fa67..89a8dd95d3c32e2a099c604b2e3c707e6c03405d 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -327,4 +327,12 @@ <!-- Whether wifi is always requested by default. --> <bool name="def_enable_wifi_always_requested">false</bool> + + <!-- Default for Settings.BATTERY_CHARGING_STATE_UPDATE_DELAY in millisecond. + -1 means system internal default value is used. --> + <integer name="def_battery_charging_state_update_delay_ms">-1</integer> + + <!-- Default for Settings.BATTERY_CHARGING_STATE_ENFORCE_LEVEL. + -1 means system internal default value is used. --> + <integer name="def_battery_charging_state_enforce_level">-1</integer> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 785003a355fc8c83a4199e6a051698343e842f11..b0abf92ffe08c83d6d61386866a33ce9e6bcbc0a 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3792,7 +3792,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 223; + private static final int SETTINGS_VERSION = 224; private final int mUserId; @@ -5965,8 +5965,23 @@ public class SettingsProvider extends ContentProvider { // Do nothing. Leave the value as is. } } + } + + currentVersion = 223; - currentVersion = 223; + // Version 223: make charging constraint update criteria customizable. + if (currentVersion == 223) { + initGlobalSettingsDefaultValLocked( + Global.BATTERY_CHARGING_STATE_UPDATE_DELAY, + getContext().getResources().getInteger( + R.integer.def_battery_charging_state_update_delay_ms)); + + initGlobalSettingsDefaultValLocked( + Global.BATTERY_CHARGING_STATE_ENFORCE_LEVEL, + getContext().getResources().getInteger( + R.integer.def_battery_charging_state_enforce_level) + ); + currentVersion = 224; } // vXXX: Add new settings above this point. diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 1f171ba4834346ab7e3d27298e2df3ba54265782..2e174e267bde377f6e1abf74c5029419afc22224 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -145,6 +145,7 @@ public class SettingsBackupTest { Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS, Settings.Global.AUTO_TIME_ZONE_EXPLICIT, Settings.Global.AVERAGE_TIME_TO_DISCHARGE, + Settings.Global.BATTERY_CHARGING_STATE_ENFORCE_LEVEL, Settings.Global.BATTERY_CHARGING_STATE_UPDATE_DELAY, Settings.Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME, Settings.Global.BROADCAST_BG_CONSTANTS, diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index c27491107ad5a0d942083c2257c3e660a09ae9f4..069ba6cd02aa51812c2dc118c24916464287a90a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -52,6 +52,14 @@ flag { bug: "283121968" } +flag { + name: "keyguard_bottom_area_refactor" + namespace: "systemui" + description: "Bottom area of keyguard refactor move into KeyguardRootView. Includes " + "lock icon and others." + bug: "290652751" +} + flag { name: "visual_interruptions_refactor" namespace: "systemui" @@ -66,3 +74,11 @@ flag { description: "Adds haptic feedback to the brightness slider." bug: "296467915" } + +flag { + name: "keyguard_shade_migration_nssl" + namespace: "systemui" + description: "Moves NSSL into a shared element between the notification_panel and " + "keyguard_root_view." + bug: "278054201" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index f2b7b3290f968df8877beb2365ca2735191f303c..56970d7e3005690ba5601cd77735e91cae34396d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog import android.app.Dialog import android.content.DialogInterface +import android.content.res.Configuration import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap @@ -50,6 +51,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -63,6 +65,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource @@ -136,7 +139,7 @@ private fun SceneScope.BouncerScene( modifier: Modifier = Modifier, ) { val backgroundColor = MaterialTheme.colorScheme.surface - val windowSizeClass = LocalWindowSizeClass.current + val layout = calculateLayout() Box(modifier) { Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) { @@ -146,22 +149,30 @@ private fun SceneScope.BouncerScene( val childModifier = Modifier.element(Bouncer.Elements.Content).fillMaxSize() val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible - when { - windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded -> + when (layout) { + Layout.STANDARD -> + Bouncer( + viewModel = viewModel, + dialogFactory = dialogFactory, + isUserInputAreaVisible = true, + modifier = childModifier, + ) + Layout.SIDE_BY_SIDE -> SideBySide( viewModel = viewModel, dialogFactory = dialogFactory, + isUserSwitcherVisible = isFullScreenUserSwitcherEnabled, modifier = childModifier, ) - isFullScreenUserSwitcherEnabled && - windowSizeClass.widthSizeClass == WindowWidthSizeClass.Medium -> + Layout.STACKED -> Stacked( viewModel = viewModel, dialogFactory = dialogFactory, + isUserSwitcherVisible = isFullScreenUserSwitcherEnabled, modifier = childModifier, ) - else -> - Bouncer( + Layout.SPLIT -> + Split( viewModel = viewModel, dialogFactory = dialogFactory, modifier = childModifier, @@ -178,11 +189,10 @@ private fun SceneScope.BouncerScene( private fun Bouncer( viewModel: BouncerViewModel, dialogFactory: BouncerSceneDialogFactory, + isUserInputAreaVisible: Boolean, modifier: Modifier = Modifier, ) { val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState() - val authMethodViewModel: AuthMethodBouncerViewModel? by - viewModel.authMethodViewModel.collectAsState() val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState() var dialog: Dialog? by remember { mutableStateOf(null) } @@ -204,25 +214,11 @@ private fun Bouncer( } Box(Modifier.weight(1f)) { - when (val nonNullViewModel = authMethodViewModel) { - is PinBouncerViewModel -> - PinBouncer( - viewModel = nonNullViewModel, - modifier = Modifier.align(Alignment.Center), - ) - is PasswordBouncerViewModel -> - PasswordBouncer( - viewModel = nonNullViewModel, - modifier = Modifier.align(Alignment.Center), - ) - is PatternBouncerViewModel -> - PatternBouncer( - viewModel = nonNullViewModel, - modifier = - Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) - .align(Alignment.BottomCenter), - ) - else -> Unit + if (isUserInputAreaVisible) { + UserInputArea( + viewModel = viewModel, + modifier = Modifier.align(Alignment.Center), + ) } } @@ -265,6 +261,40 @@ private fun Bouncer( } } +/** + * Renders the user input area, where the user interacts with the UI to enter their credentials. + * + * For example, this can be the pattern input area, the password text box, or pin pad. + */ +@Composable +private fun UserInputArea( + viewModel: BouncerViewModel, + modifier: Modifier = Modifier, +) { + val authMethodViewModel: AuthMethodBouncerViewModel? by + viewModel.authMethodViewModel.collectAsState() + + when (val nonNullViewModel = authMethodViewModel) { + is PinBouncerViewModel -> + PinBouncer( + viewModel = nonNullViewModel, + modifier = modifier, + ) + is PasswordBouncerViewModel -> + PasswordBouncer( + viewModel = nonNullViewModel, + modifier = modifier, + ) + is PatternBouncerViewModel -> + PatternBouncer( + viewModel = nonNullViewModel, + modifier = + Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false).then(modifier) + ) + else -> Unit + } +} + /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */ @Composable private fun UserSwitcher( @@ -287,62 +317,57 @@ private fun UserSwitcher( ) } - UserSwitcherDropdown( - items = dropdownItems, - ) - } -} - -@Composable -private fun UserSwitcherDropdown( - items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>, -) { - val (isDropdownExpanded, setDropdownExpanded) = remember { mutableStateOf(false) } - - items.firstOrNull()?.let { firstDropdownItem -> - Spacer(modifier = Modifier.height(40.dp)) + val (isDropdownExpanded, setDropdownExpanded) = remember { mutableStateOf(false) } + + dropdownItems.firstOrNull()?.let { firstDropdownItem -> + Spacer(modifier = Modifier.height(40.dp)) + + Box { + PlatformButton( + modifier = + Modifier + // Remove the built-in padding applied inside PlatformButton: + .padding(vertical = 0.dp) + .width(UserSwitcherDropdownWidth) + .height(UserSwitcherDropdownHeight), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + onClick = { setDropdownExpanded(!isDropdownExpanded) }, + ) { + val context = LocalContext.current + Text( + text = checkNotNull(firstDropdownItem.text.loadText(context)), + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) - Box { - PlatformButton( - modifier = - Modifier - // Remove the built-in padding applied inside PlatformButton: - .padding(vertical = 0.dp) - .width(UserSwitcherDropdownWidth) - .height(UserSwitcherDropdownHeight), - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, - contentColor = MaterialTheme.colorScheme.onSurface, - ), - onClick = { setDropdownExpanded(!isDropdownExpanded) }, - ) { - val context = LocalContext.current - Text( - text = checkNotNull(firstDropdownItem.text.loadText(context)), - style = MaterialTheme.typography.headlineSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Spacer(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + } - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(32.dp), + UserSwitcherDropdownMenu( + isExpanded = isDropdownExpanded, + items = dropdownItems, + onDismissed = { setDropdownExpanded(false) }, ) } - - UserSwitcherDropdownMenu( - isExpanded = isDropdownExpanded, - items = items, - onDismissed = { setDropdownExpanded(false) }, - ) } } } +/** + * Renders the dropdowm menu that displays the actual users and/or user actions that can be + * selected. + */ @Composable private fun UserSwitcherDropdownMenu( isExpanded: Boolean, @@ -396,19 +421,47 @@ private fun UserSwitcherDropdownMenu( } /** - * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap - * anywhere on the background to flip their positions. + * Renders the bouncer UI in split mode, with half on one side and half on the other side, swappable + * by double-tapping on the side. */ @Composable -private fun SideBySide( +private fun Split( viewModel: BouncerViewModel, dialogFactory: BouncerSceneDialogFactory, modifier: Modifier = Modifier, +) { + SwappableLayout( + startContent = { startContentModifier -> + Bouncer( + viewModel = viewModel, + dialogFactory = dialogFactory, + isUserInputAreaVisible = false, + modifier = startContentModifier, + ) + }, + endContent = { endContentModifier -> + UserInputArea( + viewModel = viewModel, + modifier = endContentModifier, + ) + }, + modifier = modifier + ) +} + +/** + * Arranges the given two contents side-by-side, supporting a double tap anywhere on the background + * to flip their positions. + */ +@Composable +private fun SwappableLayout( + startContent: @Composable (Modifier) -> Unit, + endContent: @Composable (Modifier) -> Unit, + modifier: Modifier = Modifier, ) { val layoutDirection = LocalLayoutDirection.current val isLeftToRight = layoutDirection == LayoutDirection.Ltr - val (isUserSwitcherFirst, setUserSwitcherFirst) = - rememberSaveable(isLeftToRight) { mutableStateOf(isLeftToRight) } + val (isSwapped, setSwapped) = rememberSaveable(isLeftToRight) { mutableStateOf(!isLeftToRight) } Row( modifier = @@ -416,9 +469,8 @@ private fun SideBySide( detectTapGestures( onDoubleTap = { offset -> // Depending on where the user double tapped, switch the elements such that - // the bouncer contents element is closer to the side that was double - // tapped. - setUserSwitcherFirst(offset.x > size.width / 2) + // the endContent is closer to the side that was double tapped. + setSwapped(offset.x < size.width / 2) } ) }, @@ -426,39 +478,30 @@ private fun SideBySide( val animatedOffset by animateFloatAsState( targetValue = - if (isUserSwitcherFirst) { - // When the user switcher is first, both elements have their natural - // placement so they are not offset in any way. + if (!isSwapped) { + // When startContent is first, both elements have their natural placement so + // they are not offset in any way. 0f } else if (isLeftToRight) { - // Since the user switcher is not first, the elements have to be swapped - // horizontally. In the case of LTR locales, this means pushing the user - // switcher to the right, hence the positive number. + // Since startContent is not first, the elements have to be swapped + // horizontally. In the case of LTR locales, this means pushing startContent + // to the right, hence the positive number. 1f } else { - // Since the user switcher is not first, the elements have to be swapped - // horizontally. In the case of RTL locale, this means pushing the user - // switcher to the left, hence the negative number. + // Since startContent is not first, the elements have to be swapped + // horizontally. In the case of RTL locales, this means pushing startContent + // to the left, hence the negative number. -1f }, label = "offset", ) - val userSwitcherModifier = + startContent( Modifier.fillMaxHeight().weight(1f).graphicsLayer { translationX = size.width * animatedOffset alpha = animatedAlpha(animatedOffset) } - if (viewModel.isUserSwitcherVisible) { - UserSwitcher( - viewModel = viewModel, - modifier = userSwitcherModifier, - ) - } else { - Box( - modifier = userSwitcherModifier, - ) - } + ) Box( modifier = @@ -469,41 +512,124 @@ private fun SideBySide( alpha = animatedAlpha(animatedOffset) } ) { + endContent(Modifier.widthIn(max = 400.dp).align(Alignment.BottomCenter)) + } + } +} + +/** + * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap + * anywhere on the background to flip their positions. + */ +@Composable +private fun SideBySide( + viewModel: BouncerViewModel, + dialogFactory: BouncerSceneDialogFactory, + isUserSwitcherVisible: Boolean, + modifier: Modifier = Modifier, +) { + SwappableLayout( + startContent = { startContentModifier -> + if (isUserSwitcherVisible) { + UserSwitcher( + viewModel = viewModel, + modifier = startContentModifier, + ) + } else { + Box( + modifier = startContentModifier, + ) + } + }, + endContent = { endContentModifier -> Bouncer( viewModel = viewModel, dialogFactory = dialogFactory, - modifier = Modifier.widthIn(max = 400.dp).align(Alignment.BottomCenter), + isUserInputAreaVisible = true, + modifier = endContentModifier, ) - } - } + }, + modifier = modifier, + ) } -/** Arranges the bouncer contents and user switcher contents one on top of the other. */ +/** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */ @Composable private fun Stacked( viewModel: BouncerViewModel, dialogFactory: BouncerSceneDialogFactory, + isUserSwitcherVisible: Boolean, modifier: Modifier = Modifier, ) { Column( modifier = modifier, ) { - UserSwitcher( - viewModel = viewModel, - modifier = Modifier.fillMaxWidth().weight(1f), - ) + if (isUserSwitcherVisible) { + UserSwitcher( + viewModel = viewModel, + modifier = Modifier.fillMaxWidth().weight(1f), + ) + } + Bouncer( viewModel = viewModel, dialogFactory = dialogFactory, + isUserInputAreaVisible = true, modifier = Modifier.fillMaxWidth().weight(1f), ) } } +@Composable +private fun calculateLayout(): Layout { + val windowSizeClass = LocalWindowSizeClass.current + val width = windowSizeClass.widthSizeClass + val height = windowSizeClass.heightSizeClass + val isLarge = width > WindowWidthSizeClass.Compact && height > WindowHeightSizeClass.Compact + val isTall = + when (height) { + WindowHeightSizeClass.Expanded -> width < WindowWidthSizeClass.Expanded + WindowHeightSizeClass.Medium -> width < WindowWidthSizeClass.Medium + else -> false + } + val isSquare = + when (width) { + WindowWidthSizeClass.Compact -> height == WindowHeightSizeClass.Compact + WindowWidthSizeClass.Medium -> height == WindowHeightSizeClass.Medium + WindowWidthSizeClass.Expanded -> height == WindowHeightSizeClass.Expanded + else -> false + } + val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + + return when { + // Small and tall devices (i.e. phone/folded in portrait) or square device not in landscape + // mode (unfolded with hinge along horizontal plane). + (!isLarge && isTall) || (isSquare && !isLandscape) -> Layout.STANDARD + // Small and wide devices (i.e. phone/folded in landscape). + !isLarge -> Layout.SPLIT + // Large and tall devices (i.e. tablet in portrait). + isTall -> Layout.STACKED + // Large and wide/square devices (i.e. tablet in landscape, unfolded). + else -> Layout.SIDE_BY_SIDE + } +} + interface BouncerSceneDialogFactory { operator fun invoke(): AlertDialog } +/** Enumerates all known adaptive layout configurations. */ +private enum class Layout { + /** The default UI with the bouncer laid out normally. */ + STANDARD, + /** The bouncer is displayed vertically stacked with the user switcher. */ + STACKED, + /** The bouncer is displayed side-by-side with the user switcher or an empty space. */ + SIDE_BY_SIDE, + /** The bouncer is split in two with both sides shown side-by-side. */ + SPLIT, +} + /** * Calculates an alpha for the user switcher and bouncer such that it's at `1` when the offset of * the two reaches a stopping point but `0` in the middle of the transition. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 46d418a03c00a9defcae0ffb612bf690abe6d042..37804682ed982399f997222b447cc5bcb3d964aa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,12 +30,9 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.transitions +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalViewModel - -object Scenes { - val Blank = SceneKey(name = "blank") - val Communal = SceneKey(name = "communal") -} +import kotlinx.coroutines.flow.transform object Communal { object Elements { @@ -43,7 +41,7 @@ object Communal { } val sceneTransitions = transitions { - from(Scenes.Blank, to = Scenes.Communal) { + from(TransitionSceneKey.Blank, to = TransitionSceneKey.Communal) { spec = tween(durationMillis = 500) translate(Communal.Elements.Content, Edge.Right) @@ -58,8 +56,14 @@ val sceneTransitions = transitions { * handling and transitions before the full Flexiglass layout is ready. */ @Composable -fun CommunalContainer(modifier: Modifier = Modifier, viewModel: CommunalViewModel) { - val (currentScene, setCurrentScene) = remember { mutableStateOf(Scenes.Blank) } +fun CommunalContainer( + modifier: Modifier = Modifier, + viewModel: CommunalViewModel, +) { + val currentScene: SceneKey by + viewModel.currentScene + .transform<CommunalSceneKey, SceneKey> { value -> value.toTransitionSceneKey() } + .collectAsState(TransitionSceneKey.Blank) // Failsafe to hide the whole SceneTransitionLayout in case of bugginess. var showSceneTransitionLayout by remember { mutableStateOf(true) } @@ -70,16 +74,19 @@ fun CommunalContainer(modifier: Modifier = Modifier, viewModel: CommunalViewMode SceneTransitionLayout( modifier = modifier.fillMaxSize(), currentScene = currentScene, - onChangeScene = setCurrentScene, + onChangeScene = { sceneKey -> viewModel.onSceneChanged(sceneKey.toCommunalSceneKey()) }, transitions = sceneTransitions, ) { - scene(Scenes.Blank, userActions = mapOf(Swipe.Left to Scenes.Communal)) { + scene( + TransitionSceneKey.Blank, + userActions = mapOf(Swipe.Left to TransitionSceneKey.Communal) + ) { BlankScene { showSceneTransitionLayout = false } } scene( - Scenes.Communal, - userActions = mapOf(Swipe.Right to Scenes.Blank), + TransitionSceneKey.Communal, + userActions = mapOf(Swipe.Right to TransitionSceneKey.Blank), ) { CommunalScene(viewModel, modifier = modifier) } @@ -121,3 +128,17 @@ private fun SceneScope.CommunalScene( ) { Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) } } + +// TODO(b/293899074): Remove these conversions once Compose can be used throughout SysUI. +object TransitionSceneKey { + val Blank = CommunalSceneKey.Blank.toTransitionSceneKey() + val Communal = CommunalSceneKey.Communal.toTransitionSceneKey() +} + +fun CommunalSceneKey.toTransitionSceneKey(): SceneKey { + return SceneKey(name = toString(), identity = this) +} + +fun SceneKey.toCommunalSceneKey(): CommunalSceneKey { + return this.identity as CommunalSceneKey +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 2e93a09deb300fa9a1eedff20368946e1b0c5e26..0da562bcb3bbda6c713420976f22745e3e973f51 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -161,7 +161,8 @@ private fun SceneTransitionObservableTransitionState.toModel(): ObservableTransi fromScene = fromScene.toModel().key, toScene = toScene.toModel().key, progress = progress, - isUserInputDriven = isUserInputDriven, + isInitiatedByUserInput = isInitiatedByUserInput, + isUserInputOngoing = isUserInputOngoing, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 88944f10eab9e79084694fd203c2c7b0f50e8fe6..199832bc4ab6a0a5b13adde503ade44a1aa9dbb6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -108,7 +108,7 @@ private fun CoroutineScope.animate( ) { val fromScene = layoutImpl.state.transitionState.currentScene val isUserInput = - (layoutImpl.state.transitionState as? TransitionState.Transition)?.isUserInputDriven + (layoutImpl.state.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput ?: false val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec @@ -119,9 +119,23 @@ private fun CoroutineScope.animate( val targetProgress = if (reversed) 0f else 1f val transition = if (reversed) { - OneOffTransition(target, fromScene, currentScene = target, isUserInput, animatable) + OneOffTransition( + fromScene = target, + toScene = fromScene, + currentScene = target, + isInitiatedByUserInput = isUserInput, + isUserInputOngoing = false, + animatable = animatable, + ) } else { - OneOffTransition(fromScene, target, currentScene = target, isUserInput, animatable) + OneOffTransition( + fromScene = fromScene, + toScene = target, + currentScene = target, + isInitiatedByUserInput = isUserInput, + isUserInputOngoing = false, + animatable = animatable, + ) } // Change the current layout state to use this new transition. @@ -142,7 +156,8 @@ private class OneOffTransition( override val fromScene: SceneKey, override val toScene: SceneKey, override val currentScene: SceneKey, - override val isUserInputDriven: Boolean, + override val isInitiatedByUserInput: Boolean, + override val isUserInputOngoing: Boolean, private val animatable: Animatable<Float, AnimationVector1D>, ) : TransitionState.Transition { override val progress: Float diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt new file mode 100644 index 0000000000000000000000000000000000000000..82d4239d7eb54a14e77a018279c0b49b8629ed95 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt @@ -0,0 +1,75 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + +interface EdgeDetector { + /** + * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given + * [density] and [orientation]. + */ + fun edge( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): Edge? +} + +val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp) + +/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */ +class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector { + override fun edge( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): Edge? { + val axisSize: Int + val axisPosition: Int + val topOrLeft: Edge + val bottomOrRight: Edge + when (orientation) { + Orientation.Horizontal -> { + axisSize = layoutSize.width + axisPosition = position.x + topOrLeft = Edge.Left + bottomOrRight = Edge.Right + } + Orientation.Vertical -> { + axisSize = layoutSize.height + axisPosition = position.y + topOrLeft = Edge.Top + bottomOrRight = Edge.Bottom + } + } + + val sizePx = with(density) { size.toPx() } + return when { + axisPosition <= sizePx -> topOrLeft + axisPosition >= axisSize - sizePx -> bottomOrRight + else -> null + } + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt index d005413fcbcfa53df749aa1c20b226a611b16d21..ae7d8f599b9143ff726aeed344457b72d6550c70 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt @@ -2,7 +2,6 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import kotlinx.coroutines.CoroutineScope interface GestureHandler { val draggable: DraggableHandler @@ -10,9 +9,9 @@ interface GestureHandler { } interface DraggableHandler { - suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) + fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1) fun onDelta(pixels: Float) - suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) + fun onDragStopped(velocity: Float) } interface NestedScrollHandler { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt new file mode 100644 index 0000000000000000000000000000000000000000..97d3fff48b23f3d30ff923b592922f450057e934 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -0,0 +1,191 @@ +/* + * 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.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation +import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation +import androidx.compose.foundation.gestures.horizontalDrag +import androidx.compose.foundation.gestures.verticalDrag +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.input.pointer.util.addPointerInputChange +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.util.fastForEach + +/** + * Make an element draggable in the given [orientation]. + * + * The main difference with [multiPointerDraggable] and + * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number + * of pointers that are down when the drag is started. If you don't need this information, you + * should use `draggable` instead. + * + * Note that the current implementation is trivial: we wait for the touch slope on the *first* down + * pointer, then we count the number of distinct pointers that are down right before calling + * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not + * dragged) and a second pointer is down and dragged. This is an implementation detail that might + * change in the future. + */ +// TODO(b/291055080): Migrate to the Modifier.Node API. +@Composable +internal fun Modifier.multiPointerDraggable( + orientation: Orientation, + enabled: Boolean, + startDragImmediately: Boolean, + onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit, + onDragDelta: (Float) -> Unit, + onDragStopped: (velocity: Float) -> Unit, +): Modifier { + val onDragStarted by rememberUpdatedState(onDragStarted) + val onDragStopped by rememberUpdatedState(onDragStopped) + val onDragDelta by rememberUpdatedState(onDragDelta) + val startDragImmediately by rememberUpdatedState(startDragImmediately) + + val velocityTracker = remember { VelocityTracker() } + val maxFlingVelocity = + LocalViewConfiguration.current.maximumFlingVelocity.let { max -> + val maxF = max.toFloat() + Velocity(maxF, maxF) + } + + return this.pointerInput(enabled, orientation, maxFlingVelocity) { + if (!enabled) { + return@pointerInput + } + + val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown -> + velocityTracker.resetTracking() + onDragStarted(startedPosition, pointersDown) + } + + val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) } + + val onDragEnd: () -> Unit = { + val velocity = velocityTracker.calculateVelocity(maxFlingVelocity) + onDragStopped( + when (orientation) { + Orientation.Horizontal -> velocity.x + Orientation.Vertical -> velocity.y + } + ) + } + + val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount -> + velocityTracker.addPointerInputChange(change) + onDragDelta(amount) + } + + detectDragGestures( + orientation = orientation, + startDragImmediately = { startDragImmediately }, + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onDrag = onDrag, + ) + } +} + +/** + * Detect drag gestures in the given [orientation]. + * + * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and + * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for: + * 1) starting the gesture immediately without requiring a drag >= touch slope; + * 2) passing the number of pointers down to [onDragStart]. + */ +private suspend fun PointerInputScope.detectDragGestures( + orientation: Orientation, + startDragImmediately: () -> Boolean, + onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit, + onDragEnd: () -> Unit, + onDragCancel: () -> Unit, + onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, +) { + awaitEachGesture { + val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + var overSlop = 0f + val drag = + if (startDragImmediately()) { + initialDown.consume() + initialDown + } else { + val down = awaitFirstDown(requireUnconsumed = false) + val onSlopReached = { change: PointerInputChange, over: Float -> + change.consume() + overSlop = over + } + + // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once + // it is public. + when (orientation) { + Orientation.Horizontal -> + awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached) + Orientation.Vertical -> + awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached) + } + } + + if (drag != null) { + // Count the number of pressed pointers. + val pressed = mutableSetOf<PointerId>() + currentEvent.changes.fastForEach { change -> + if (change.pressed) { + pressed.add(change.id) + } + } + + onDragStart(drag.position, pressed.size) + onDrag(drag, overSlop) + + val successful = + when (orientation) { + Orientation.Horizontal -> + horizontalDrag(drag.id) { + onDrag(it, it.positionChange().x) + it.consume() + } + Orientation.Vertical -> + verticalDrag(drag.id) { + onDrag(it, it.positionChange().y) + it.consume() + } + } + + if (successful) { + onDragEnd() + } else { + onDragCancel() + } + } + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt index ccdec6ea8c5e525ff3f97de0ce490011b4bc352a..1b79dbdee5104eafc7b3a321773c30e9b098b229 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt @@ -52,7 +52,14 @@ sealed class ObservableTransitionState { * scene, this value will remain true after the pointer is no longer touching the screen and * will be true in any transition created to animate back to the original position. */ - val isUserInputDriven: Boolean, + val isInitiatedByUserInput: Boolean, + + /** + * Whether user input is currently driving the transition. For example, if a user is + * dragging a pointer, this emits true. Once they lift their finger, this emits false while + * the transition completes/settles. + */ + val isUserInputOngoing: Flow<Boolean>, ) : ObservableTransitionState() } @@ -73,7 +80,8 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans fromScene = state.fromScene, toScene = state.toScene, progress = snapshotFlow { state.progress }, - isUserInputDriven = state.isUserInputDriven, + isInitiatedByUserInput = state.isInitiatedByUserInput, + isUserInputOngoing = snapshotFlow { state.isUserInputOngoing }, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index 9c799b28257184bc4999bd1d647d383abce4660a..3fd6828fca6b093e8b9564eab0ebf4b9261058c2 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -16,7 +16,6 @@ package com.android.compose.animation.scene -import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -101,19 +100,3 @@ private class SceneScopeImpl( MovableElement(layoutImpl, scene, key, modifier, content) } } - -/** The destination scene when swiping up or left from [upOrLeft]. */ -internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? { - return when (orientation) { - Orientation.Vertical -> userActions[Swipe.Up] - Orientation.Horizontal -> userActions[Swipe.Left] - } -} - -/** The destination scene when swiping down or right from [downOrRight]. */ -internal fun Scene.downOrRight(orientation: Orientation): SceneKey? { - return when (orientation) { - Orientation.Vertical -> userActions[Swipe.Down] - Orientation.Horizontal -> userActions[Swipe.Right] - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 74e66d2a99494ef7e9fd85dc631f3751f34fd061..1f38e70799c3c33bd8ae4f30302c7afde90c362b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember @@ -37,6 +38,7 @@ import androidx.compose.ui.platform.LocalDensity * instance by triggering back navigation or by swiping to a new scene. * @param transitions the definition of the transitions used to animate a change of scene. * @param state the observable state of this layout. + * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any. * @param scenes the configuration of the different scenes of this layout. */ @Composable @@ -46,6 +48,7 @@ fun SceneTransitionLayout( transitions: SceneTransitions, modifier: Modifier = Modifier, state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) }, + edgeDetector: EdgeDetector = DefaultEdgeDetector, scenes: SceneTransitionLayoutScope.() -> Unit, ) { val density = LocalDensity.current @@ -56,15 +59,17 @@ fun SceneTransitionLayout( transitions, state, density, + edgeDetector, ) } layoutImpl.onChangeScene = onChangeScene layoutImpl.transitions = transitions layoutImpl.density = density + layoutImpl.edgeDetector = edgeDetector + layoutImpl.setScenes(scenes) layoutImpl.setCurrentScene(currentScene) - layoutImpl.Content(modifier) } @@ -191,9 +196,9 @@ data class Swipe( } } -enum class SwipeDirection { - Up, - Down, - Left, - Right, +enum class SwipeDirection(val orientation: Orientation) { + Up(Orientation.Vertical), + Down(Orientation.Vertical), + Left(Orientation.Horizontal), + Right(Orientation.Horizontal), } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index a40b299918770be98aa038491d17ad5d7ffa4f9d..fd6265999b07ec51c1ee7ae22becd28065590625 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.layout.LookaheadScope import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize -import com.android.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEach import kotlinx.coroutines.channels.Channel @VisibleForTesting @@ -47,6 +47,7 @@ class SceneTransitionLayoutImpl( transitions: SceneTransitions, internal val state: SceneTransitionLayoutState, density: Density, + edgeDetector: EdgeDetector, ) { internal val scenes = SnapshotStateMap<SceneKey, Scene>() internal val elements = SnapshotStateMap<ElementKey, Element>() @@ -57,6 +58,7 @@ class SceneTransitionLayoutImpl( internal var onChangeScene by mutableStateOf(onChangeScene) internal var transitions by mutableStateOf(transitions) internal var density: Density by mutableStateOf(density) + internal var edgeDetector by mutableStateOf(edgeDetector) /** * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 7a21211c3ddeada5408d27c59620c34ede5c9306..b9f83c545122ede4b7b37c8e3346a97e3629567c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -70,6 +70,9 @@ sealed interface TransitionState { val progress: Float /** Whether the transition was triggered by user input rather than being programmatic. */ - val isUserInputDriven: Boolean + val isInitiatedByUserInput: Boolean + + /** Whether user input is currently driving the transition. */ + val isUserInputOngoing: Boolean } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index 75dcb2e44c135befcff683741a796cdde1491ad0..b163a2a96039801e95804761254eb074ebc7448e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -21,6 +21,8 @@ import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.snap import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap import com.android.compose.animation.scene.transformation.AnchoredSize import com.android.compose.animation.scene.transformation.AnchoredTranslate import com.android.compose.animation.scene.transformation.EdgeTranslate @@ -32,8 +34,6 @@ import com.android.compose.animation.scene.transformation.ScaleSize import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.animation.scene.transformation.Transformation import com.android.compose.animation.scene.transformation.Translate -import com.android.compose.ui.util.fastForEach -import com.android.compose.ui.util.fastMap /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 2dc53ab8bf76e4ae91fa9a6892f20f508de42b6e..877ac095af4e4f6eb01d579cd6f42cb164ad6e49 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -22,8 +22,6 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -37,6 +35,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round import com.android.compose.nestedscroll.PriorityNestedScrollConnection import kotlin.math.absoluteValue import kotlinx.coroutines.CoroutineScope @@ -55,7 +54,7 @@ internal fun Modifier.swipeToScene( /** Whether swipe should be enabled in the given [orientation]. */ fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean = - upOrLeft(orientation) != null || downOrRight(orientation) != null + userActions.keys.any { it is Swipe && it.direction.orientation == orientation } val currentScene = gestureHandler.currentScene val canSwipe = currentScene.shouldEnableSwipes(orientation) @@ -68,8 +67,7 @@ internal fun Modifier.swipeToScene( ) return nestedScroll(connection = gestureHandler.nestedScroll.connection) - .draggable( - state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta), + .multiPointerDraggable( orientation = orientation, enabled = gestureHandler.isDrivingTransition || canSwipe, // Immediately start the drag if this our [transition] is currently animating to a scene @@ -80,6 +78,7 @@ internal fun Modifier.swipeToScene( gestureHandler.isAnimatingOffset && !canOppositeSwipe, onDragStarted = gestureHandler.draggable::onDragStarted, + onDragDelta = gestureHandler.draggable::onDelta, onDragStopped = gestureHandler.draggable::onDragStopped, ) } @@ -159,7 +158,7 @@ class SceneGestureHandler( internal var gestureWithPriority: Any? = null - internal fun onDragStarted() { + internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) { if (isDrivingTransition) { // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. @@ -199,6 +198,48 @@ class SceneGestureHandler( Orientation.Vertical -> layoutImpl.size.height }.toFloat() + val fromEdge = + startedPosition?.let { position -> + layoutImpl.edgeDetector.edge( + layoutImpl.size, + position.round(), + layoutImpl.density, + orientation, + ) + } + + swipeTransition.actionUpOrLeft = + Swipe( + direction = + when (orientation) { + Orientation.Horizontal -> SwipeDirection.Left + Orientation.Vertical -> SwipeDirection.Up + }, + pointerCount = pointersDown, + fromEdge = fromEdge, + ) + + swipeTransition.actionDownOrRight = + Swipe( + direction = + when (orientation) { + Orientation.Horizontal -> SwipeDirection.Right + Orientation.Vertical -> SwipeDirection.Down + }, + pointerCount = pointersDown, + fromEdge = fromEdge, + ) + + if (fromEdge == null) { + swipeTransition.actionUpOrLeftNoEdge = null + swipeTransition.actionDownOrRightNoEdge = null + } else { + swipeTransition.actionUpOrLeftNoEdge = + (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null) + swipeTransition.actionDownOrRightNoEdge = + (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null) + } + if (swipeTransition.absoluteDistance > 0f) { transitionState = swipeTransition } @@ -246,11 +287,11 @@ class SceneGestureHandler( // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset val absoluteDistance = swipeTransition.absoluteDistance - if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) { + if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) { swipeTransition.dragOffset += absoluteDistance swipeTransition._fromScene = toScene } else if ( - offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key + offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key ) { swipeTransition.dragOffset -= absoluteDistance swipeTransition._fromScene = toScene @@ -266,27 +307,21 @@ class SceneGestureHandler( ) private fun Scene.findTargetSceneAndDistance(directionOffset: Float): TargetScene { - val maxDistance = - when (orientation) { - Orientation.Horizontal -> layoutImpl.size.width - Orientation.Vertical -> layoutImpl.size.height - }.toFloat() - - val upOrLeft = upOrLeft(orientation) - val downOrRight = downOrRight(orientation) + val upOrLeft = swipeTransition.upOrLeft(this) + val downOrRight = swipeTransition.downOrRight(this) // Compute the target scene depending on the current offset. return when { directionOffset < 0f && upOrLeft != null -> { TargetScene( sceneKey = upOrLeft, - distance = -maxDistance, + distance = -swipeTransition.absoluteDistance, ) } directionOffset > 0f && downOrRight != null -> { TargetScene( sceneKey = downOrRight, - distance = maxDistance, + distance = swipeTransition.absoluteDistance, ) } else -> { @@ -473,7 +508,7 @@ class SceneGestureHandler( return offset / distance } - override val isUserInputDriven = true + override val isInitiatedByUserInput = true /** The current offset caused by the drag gesture. */ var dragOffset by mutableFloatStateOf(0f) @@ -484,6 +519,10 @@ class SceneGestureHandler( */ var isAnimatingOffset by mutableStateOf(false) + // If we are not animating offset, it means the offset is being driven by the user's finger. + override val isUserInputOngoing: Boolean + get() = !isAnimatingOffset + /** The animatable used to animate the offset once the user lifted its finger. */ val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold) @@ -516,6 +555,22 @@ class SceneGestureHandler( var _distance by mutableFloatStateOf(0f) val distance: Float get() = _distance + + /** The [UserAction]s associated to this swipe. */ + var actionUpOrLeft: UserAction = Back + var actionDownOrRight: UserAction = Back + var actionUpOrLeftNoEdge: UserAction? = null + var actionDownOrRightNoEdge: UserAction? = null + + fun upOrLeft(scene: Scene): SceneKey? { + return scene.userActions[actionUpOrLeft] + ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] } + } + + fun downOrRight(scene: Scene): SceneKey? { + return scene.userActions[actionDownOrRight] + ?: actionDownOrRightNoEdge?.let { scene.userActions[it] } + } } companion object { @@ -526,9 +581,9 @@ class SceneGestureHandler( private class SceneDraggableHandler( private val gestureHandler: SceneGestureHandler, ) : DraggableHandler { - override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) { + override fun onDragStarted(startedPosition: Offset, pointersDown: Int) { gestureHandler.gestureWithPriority = this - gestureHandler.onDragStarted() + gestureHandler.onDragStarted(pointersDown, startedPosition) } override fun onDelta(pixels: Float) { @@ -537,7 +592,7 @@ private class SceneDraggableHandler( } } - override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) { + override fun onDragStopped(velocity: Float) { if (gestureHandler.gestureWithPriority == this) { gestureHandler.gestureWithPriority = null gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true) @@ -580,11 +635,31 @@ class SceneNestedScrollHandler( // moving on to the next scene. var gestureStartedOnNestedChild = false + val actionUpOrLeft = + Swipe( + direction = + when (gestureHandler.orientation) { + Orientation.Horizontal -> SwipeDirection.Left + Orientation.Vertical -> SwipeDirection.Up + }, + pointerCount = 1, + ) + + val actionDownOrRight = + Swipe( + direction = + when (gestureHandler.orientation) { + Orientation.Horizontal -> SwipeDirection.Right + Orientation.Vertical -> SwipeDirection.Down + }, + pointerCount = 1, + ) + fun findNextScene(amount: Float): SceneKey? { val fromScene = gestureHandler.currentScene return when { - amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation) - amount > 0f -> fromScene.downOrRight(gestureHandler.orientation) + amount < 0f -> fromScene.userActions[actionUpOrLeft] + amount > 0f -> fromScene.userActions[actionDownOrRight] else -> null } } @@ -625,7 +700,7 @@ class SceneNestedScrollHandler( onStart = { gestureHandler.gestureWithPriority = this priorityScene = nextScene - gestureHandler.onDragStarted() + gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null) }, onScroll = { offsetAvailable -> if (gestureHandler.gestureWithPriority != this) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/ListUtils.kt b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/ListUtils.kt deleted file mode 100644 index 741f00d9f19b70976f772fe91b48e4f855b02e3a..0000000000000000000000000000000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/ListUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.ui.util - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -/** - * Iterates through a [List] using the index and calls [action] for each item. This does not - * allocate an iterator like [Iterable.forEach]. - * - * **Do not use for collections that come from public APIs**, since they may not support random - * access in an efficient way, and this method may actually be a lot slower. Only use for - * collections that are created by code we control and are known to support random access. - */ -@Suppress("BanInlineOptIn") -@OptIn(ExperimentalContracts::class) -internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) { - contract { callsInPlace(action) } - for (index in indices) { - val item = get(index) - action(item) - } -} - -/** - * Returns a list containing the results of applying the given [transform] function to each element - * in the original collection. - * - * **Do not use for collections that come from public APIs**, since they may not support random - * access in an efficient way, and this method may actually be a lot slower. Only use for - * collections that are created by code we control and are known to support random access. - */ -@Suppress("BanInlineOptIn") -@OptIn(ExperimentalContracts::class) -internal inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> { - contract { callsInPlace(transform) } - val target = ArrayList<R>(size) - fastForEach { target += transform(it) } - return target -} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a68282ae78f4629eb9698d9de98bc75fbe626223 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.compose.animation.scene + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FixedSizeEdgeDetectorTest { + private val detector = FixedSizeEdgeDetector(30.dp) + private val layoutSize = IntSize(100, 100) + private val density = Density(1f) + + @Test + fun horizontalEdges() { + fun horizontalEdge(position: Int): Edge? = + detector.edge( + layoutSize, + position = IntOffset(position, 0), + density, + Orientation.Horizontal, + ) + + assertThat(horizontalEdge(0)).isEqualTo(Edge.Left) + assertThat(horizontalEdge(30)).isEqualTo(Edge.Left) + assertThat(horizontalEdge(31)).isEqualTo(null) + assertThat(horizontalEdge(69)).isEqualTo(null) + assertThat(horizontalEdge(70)).isEqualTo(Edge.Right) + assertThat(horizontalEdge(100)).isEqualTo(Edge.Right) + } + + @Test + fun verticalEdges() { + fun verticalEdge(position: Int): Edge? = + detector.edge( + layoutSize, + position = IntOffset(0, position), + density, + Orientation.Vertical, + ) + + assertThat(verticalEdge(0)).isEqualTo(Edge.Top) + assertThat(verticalEdge(30)).isEqualTo(Edge.Top) + assertThat(verticalEdge(31)).isEqualTo(null) + assertThat(verticalEdge(69)).isEqualTo(null) + assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom) + assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index 6791a85ff21c2e41dc348ff3311171af5f70bb6b..1eb3392f1374d7248fd8fdc3496d52df2c7822a7 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -55,7 +55,8 @@ class SceneGestureHandlerTest { builder = scenesBuilder, transitions = EmptyTestTransitions, state = layoutState, - density = Density(1f) + density = Density(1f), + edgeDetector = DefaultEdgeDetector, ) .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) }, orientation = Orientation.Vertical, @@ -104,13 +105,13 @@ class SceneGestureHandlerTest { @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) val transition = transitionState as Transition @@ -123,14 +124,13 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) assertScene(currentScene = SceneA, isIdle = false) draggable.onDragStopped( - coroutineScope = coroutineScope, velocity = velocityThreshold - 0.01f, ) assertScene(currentScene = SceneA, isIdle = false) @@ -142,14 +142,13 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) assertScene(currentScene = SceneA, isIdle = false) draggable.onDragStopped( - coroutineScope = coroutineScope, velocity = velocityThreshold, ) assertScene(currentScene = SceneC, isIdle = false) @@ -161,23 +160,22 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) - draggable.onDragStopped(coroutineScope = coroutineScope, velocity = 0f) + draggable.onDragStopped(velocity = 0f) assertScene(currentScene = SceneA, isIdle = true) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) draggable.onDelta(pixels = deltaInPixels10) assertScene(currentScene = SceneA, isIdle = false) draggable.onDragStopped( - coroutineScope = coroutineScope, velocity = velocityThreshold, ) @@ -191,7 +189,7 @@ class SceneGestureHandlerTest { assertScene(currentScene = SceneC, isIdle = false) // Start a new gesture while the offset is animating - draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero) + draggable.onDragStarted(startedPosition = Offset.Zero) assertThat(sceneGestureHandler.isAnimatingOffset).isFalse() } @@ -320,7 +318,7 @@ class SceneGestureHandlerTest { } @Test fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { - draggable.onDragStopped(coroutineScope, velocityThreshold) + draggable.onDragStopped(velocityThreshold) assertScene(currentScene = SceneA, isIdle = true) } @@ -332,7 +330,7 @@ class SceneGestureHandlerTest { @Test fun startNestedScrollWhileDragging() = runGestureTest { - draggable.onDragStarted(coroutineScope, Offset.Zero) + draggable.onDragStarted(Offset.Zero) assertScene(currentScene = SceneA, isIdle = false) val transition = transitionState as Transition @@ -344,7 +342,7 @@ class SceneGestureHandlerTest { assertThat(transition.progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - draggable.onDragStopped(coroutineScope, velocityThreshold) + draggable.onDragStopped(velocityThreshold) assertScene(currentScene = SceneA, isIdle = false) nestedScrollEvents(available = offsetY10) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index df3b72aa5533f5e7fbd6356b97a3cf95e15dc540..58d853ef5a00e6bab8175596371f3a012a4efa8a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -48,6 +48,14 @@ class SwipeToSceneTest { /** The middle of the layout, in pixels. */ private val Density.middle: Offset get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx()) + + /** The middle-top of the layout, in pixels. */ + private val Density.middleTop: Offset + get() = Offset((LayoutWidth / 2).toPx(), 0f) + + /** The middle-left of the layout, in pixels. */ + private val Density.middleLeft: Offset + get() = Offset(0f, (LayoutHeight / 2).toPx()) } private var currentScene by mutableStateOf(TestScenes.SceneA) @@ -83,7 +91,13 @@ class SwipeToSceneTest { } scene( TestScenes.SceneC, - userActions = mapOf(Swipe.Down to TestScenes.SceneA), + userActions = + mapOf( + Swipe.Down to TestScenes.SceneA, + Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB, + Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB, + Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB, + ), ) { Box(Modifier.fillMaxSize()) } @@ -122,7 +136,7 @@ class SwipeToSceneTest { assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth) - assertThat(transition.isUserInputDriven).isTrue() + assertThat(transition.isInitiatedByUserInput).isTrue() // Release the finger. We should now be animating back to A (currentScene = SceneA) given // that 55dp < positional threshold. @@ -134,7 +148,7 @@ class SwipeToSceneTest { assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth) - assertThat(transition.isUserInputDriven).isTrue() + assertThat(transition.isInitiatedByUserInput).isTrue() // Wait for the animation to finish. We should now be in scene A. rule.waitForIdle() @@ -156,7 +170,7 @@ class SwipeToSceneTest { assertThat(transition.toScene).isEqualTo(TestScenes.SceneC) assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight) - assertThat(transition.isUserInputDriven).isTrue() + assertThat(transition.isInitiatedByUserInput).isTrue() // Release the finger. We should now be animating to C (currentScene = SceneC) given // that 56dp >= positional threshold. @@ -168,7 +182,7 @@ class SwipeToSceneTest { assertThat(transition.toScene).isEqualTo(TestScenes.SceneC) assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC) assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight) - assertThat(transition.isUserInputDriven).isTrue() + assertThat(transition.isInitiatedByUserInput).isTrue() // Wait for the animation to finish. We should now be in scene C. rule.waitForIdle() @@ -242,4 +256,100 @@ class SwipeToSceneTest { assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) } + + @Test + fun multiPointerSwipe() { + // Start at scene C. + currentScene = TestScenes.SceneC + + // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is + // detected as a drag event. + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + TestContent() + } + + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + + // Swipe down with two fingers. + rule.onRoot().performTouchInput { + repeat(2) { i -> down(pointerId = i, middle) } + repeat(2) { i -> + moveBy(pointerId = i, Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000) + } + } + + // We are transitioning to B because we used 2 fingers. + val transition = layoutState.transitionState + assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) + assertThat((transition as TransitionState.Transition).fromScene) + .isEqualTo(TestScenes.SceneC) + assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + + // Release the fingers and wait for the animation to end. We are back to C because we only + // swiped 10dp. + rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } } + rule.waitForIdle() + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + } + + @Test + fun defaultEdgeSwipe() { + // Start at scene C. + currentScene = TestScenes.SceneC + + // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is + // detected as a drag event. + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + TestContent() + } + + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + + // Swipe down from the top edge. + rule.onRoot().performTouchInput { + down(middleTop) + moveBy(Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000) + } + + // We are transitioning to B (and not A) because we started from the top edge. + var transition = layoutState.transitionState + assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) + assertThat((transition as TransitionState.Transition).fromScene) + .isEqualTo(TestScenes.SceneC) + assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + + // Release the fingers and wait for the animation to end. We are back to C because we only + // swiped 10dp. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + + // Swipe right from the left edge. + rule.onRoot().performTouchInput { + down(middleLeft) + moveBy(Offset(touchSlop + 10.dp.toPx(), 0f), delayMillis = 1_000) + } + + // We are transitioning to B (and not A) because we started from the left edge. + transition = layoutState.transitionState + assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) + assertThat((transition as TransitionState.Transition).fromScene) + .isEqualTo(TestScenes.SceneC) + assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + + // Release the fingers and wait for the animation to end. We are back to C because we only + // swiped 10dp. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + } } diff --git a/packages/SystemUI/res-keyguard/layout/sidefps_progress_bar.xml b/packages/SystemUI/res-keyguard/layout/sidefps_progress_bar.xml index 183f0e591c919a7843e672bc24d0bc030def399b..9d746777170432f02b0a021d51248cd52e9e9094 100644 --- a/packages/SystemUI/res-keyguard/layout/sidefps_progress_bar.xml +++ b/packages/SystemUI/res-keyguard/layout/sidefps_progress_bar.xml @@ -15,18 +15,18 @@ ~ --> -<LinearLayout android:layout_height="match_parent" +<RelativeLayout android:layout_width="match_parent" - android:orientation="vertical" - android:layoutDirection="ltr" - android:gravity="center" + android:layout_height="match_parent" + android:gravity="left|top" + android:background="@android:color/transparent" xmlns:android="http://schemas.android.com/apk/res/android"> <ProgressBar android:id="@+id/side_fps_progress_bar" - android:layout_width="55dp" - android:layout_height="10dp" + android:layout_width="0dp" + android:layout_height="@dimen/sfps_progress_bar_thickness" android:indeterminateOnly="false" android:min="0" android:max="100" android:progressDrawable="@drawable/progress_bar" /> -</LinearLayout> +</RelativeLayout> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index d1067a9960bf99280e8c50154be2f4801e0fc9a8..0628c3e957b11ed8d467e3de40dac3de1f4cdcb7 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -157,4 +157,11 @@ <dimen name="weather_clock_smartspace_translateX">0dp</dimen> <dimen name="weather_clock_smartspace_translateY">0dp</dimen> + <!-- Additional length to add to the SFPS sensor length we get from framework so that the length + of the progress bar matches the length of the power button --> + <dimen name="sfps_progress_bar_length_extra_padding">12dp</dimen> + <!-- Thickness of the progress bar we show for the SFPS based authentication. --> + <dimen name="sfps_progress_bar_thickness">6dp</dimen> + <!-- Padding from the edge of the screen for the progress bar --> + <dimen name="sfps_progress_bar_padding_from_edge">7dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4c5204447db0430aa0e345541a226ba50ad797ba..12bff4a27277b4ddec9993c8d6d6a2611488167c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1045,8 +1045,8 @@ <!-- Indication on the keyguard that is shown when the device is dock charging. [CHAR LIMIT=80]--> <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string> - <!-- Indicator shown to start the communal tutorial. [CHAR LIMIT=100] --> - <string name="communal_tutorial_indicator_text">Click on the arrow button to start the communal tutorial</string> + <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] --> + <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string> <!-- Related to user switcher --><skip/> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java index 631423e4b7fcf40cb7b33ec566ae792f2205addc..10393cf83247c169659a3b608c61832f8d60f83b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java @@ -137,14 +137,12 @@ public class ActivityManagerWrapper { } /** - * @return a {@link ThumbnailData} with {@link TaskSnapshot} for the given {@param taskId}. - * The snapshot will be triggered if no cached {@link TaskSnapshot} exists. + * @return the task snapshot for the given {@param taskId}. */ public @NonNull ThumbnailData getTaskThumbnail(int taskId, boolean isLowResolution) { TaskSnapshot snapshot = null; try { - snapshot = getService().getTaskSnapshot(taskId, isLowResolution, - true /* takeSnapshotIfNeeded */); + snapshot = getService().getTaskSnapshot(taskId, isLowResolution); } catch (RemoteException e) { Log.w(TAG, "Failed to retrieve task snapshot", e); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index e69eced0de45e0f9a14aab45f4a32c8e5e39a415..1ac4163649a433c64a9fda49a17dabc2f6b8c858 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -489,6 +489,10 @@ public class AuthContainerView extends LinearLayout public void onAttachedToWindow() { super.onAttachedToWindow(); + if (mContainerState == STATE_ANIMATING_OUT) { + return; + } + mWakefulnessLifecycle.addObserver(this); mPanelInteractionDetector.enable( () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED)); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt index 0567ea2ee465db232f0a8f030a9c888115bbbd05..f51379940640b0fbfcbf0bdf213620a885f5e4d4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt @@ -26,17 +26,17 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.log.SideFpsLogger import com.android.systemui.res.R import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach @SysUISingleton class SideFpsSensorInteractor @@ -46,12 +46,11 @@ constructor( fingerprintPropertyRepository: FingerprintPropertyRepository, windowManager: WindowManager, displayStateInteractor: DisplayStateInteractor, - featureFlags: FeatureFlagsClassic, fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>, private val logger: SideFpsLogger, ) { - private val sensorForCurrentDisplay = + private val sensorLocationForCurrentDisplay = combine( displayStateInteractor.displayChanges, fingerprintPropertyRepository.sensorLocations, @@ -63,91 +62,100 @@ constructor( val isAvailable: Flow<Boolean> = fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON } - val authenticationDuration: Flow<Long> = - flowOf(context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L) + val authenticationDuration: Long = + context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = - if ( - fingerprintInteractiveToAuthProvider.isEmpty || - !featureFlags.isEnabled(Flags.REST_TO_UNLOCK) - ) { + if (fingerprintInteractiveToAuthProvider.isEmpty) { flowOf(false) } else { combine( isAvailable, fingerprintInteractiveToAuthProvider.get().enabledForCurrentUser ) { sfpsAvailable, isSettingEnabled -> - logger.logStateChange(sfpsAvailable, isSettingEnabled) sfpsAvailable && isSettingEnabled } } val sensorLocation: Flow<SideFpsSensorLocation> = - combine(displayStateInteractor.currentRotation, sensorForCurrentDisplay, ::Pair).map { - (rotation, sensorLocation: SensorLocationInternal) -> - val isSensorVerticalInDefaultOrientation = sensorLocation.sensorLocationY != 0 - // device dimensions in the current rotation - val size = windowManager.maximumWindowMetrics.bounds - val isDefaultOrientation = rotation.isDefaultOrientation() - // Width and height are flipped is device is not in rotation_0 or rotation_180 - // Flipping it to the width and height of the device in default orientation. - val displayWidth = if (isDefaultOrientation) size.width() else size.height() - val displayHeight = if (isDefaultOrientation) size.height() else size.width() - val sensorWidth = context.resources?.getInteger(R.integer.config_sfpsSensorWidth) ?: 0 + combine(displayStateInteractor.currentRotation, sensorLocationForCurrentDisplay, ::Pair) + .map { (rotation, sensorLocation: SensorLocationInternal) -> + val isSensorVerticalInDefaultOrientation = sensorLocation.sensorLocationY != 0 + // device dimensions in the current rotation + val windowMetrics = windowManager.maximumWindowMetrics + val size = windowMetrics.bounds + val isDefaultOrientation = rotation.isDefaultOrientation() + // Width and height are flipped is device is not in rotation_0 or rotation_180 + // Flipping it to the width and height of the device in default orientation. + val displayWidth = if (isDefaultOrientation) size.width() else size.height() + val displayHeight = if (isDefaultOrientation) size.height() else size.width() + val sensorLengthInPx = sensorLocation.sensorRadius * 2 - val (sensorLeft, sensorTop) = - if (isSensorVerticalInDefaultOrientation) { - when (rotation) { - DisplayRotation.ROTATION_0 -> { - Pair(displayWidth, sensorLocation.sensorLocationY) + val (sensorLeft, sensorTop) = + if (isSensorVerticalInDefaultOrientation) { + when (rotation) { + DisplayRotation.ROTATION_0 -> { + Pair(displayWidth, sensorLocation.sensorLocationY) + } + DisplayRotation.ROTATION_90 -> { + Pair(sensorLocation.sensorLocationY, 0) + } + DisplayRotation.ROTATION_180 -> { + Pair( + 0, + displayHeight - + sensorLocation.sensorLocationY - + sensorLengthInPx + ) + } + DisplayRotation.ROTATION_270 -> { + Pair( + displayHeight - + sensorLocation.sensorLocationY - + sensorLengthInPx, + displayWidth + ) + } } - DisplayRotation.ROTATION_90 -> { - Pair(sensorLocation.sensorLocationY, 0) - } - DisplayRotation.ROTATION_180 -> { - Pair(0, displayHeight - sensorLocation.sensorLocationY - sensorWidth) - } - DisplayRotation.ROTATION_270 -> { - Pair( - displayHeight - sensorLocation.sensorLocationY - sensorWidth, - displayWidth - ) - } - } - } else { - when (rotation) { - DisplayRotation.ROTATION_0 -> { - Pair(sensorLocation.sensorLocationX, 0) - } - DisplayRotation.ROTATION_90 -> { - Pair(0, displayWidth - sensorLocation.sensorLocationX - sensorWidth) - } - DisplayRotation.ROTATION_180 -> { - Pair( - displayWidth - sensorLocation.sensorLocationX - sensorWidth, - displayHeight - ) - } - DisplayRotation.ROTATION_270 -> { - Pair(displayHeight, sensorLocation.sensorLocationX) + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> { + Pair(sensorLocation.sensorLocationX, 0) + } + DisplayRotation.ROTATION_90 -> { + Pair( + 0, + displayWidth - sensorLocation.sensorLocationX - sensorLengthInPx + ) + } + DisplayRotation.ROTATION_180 -> { + Pair( + displayWidth - + sensorLocation.sensorLocationX - + sensorLengthInPx, + displayHeight + ) + } + DisplayRotation.ROTATION_270 -> { + Pair(displayHeight, sensorLocation.sensorLocationX) + } } } - } - logger.sensorLocationStateChanged( - size, - rotation, - displayWidth, - displayHeight, - sensorWidth, - isSensorVerticalInDefaultOrientation - ) - - SideFpsSensorLocation( - left = sensorLeft, - top = sensorTop, - width = sensorWidth, - isSensorVerticalInDefaultOrientation = isSensorVerticalInDefaultOrientation - ) - } + SideFpsSensorLocation( + left = sensorLeft, + top = sensorTop, + length = sensorLengthInPx, + isSensorVerticalInDefaultOrientation = isSensorVerticalInDefaultOrientation + ) + } + .distinctUntilChanged() + .onEach { + logger.sensorLocationStateChanged( + it.left, + it.top, + it.length, + it.isSensorVerticalInDefaultOrientation + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt index 35f8e3bb461ff1152a33f360f2cc42beea457541..12f374fe34055c90303f2848f968ba574b22ce14 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt @@ -21,8 +21,8 @@ data class SideFpsSensorLocation( val left: Int, /** Pixel offset from the top of the screen */ val top: Int, - /** Width in pixels of the SFPS sensor */ - val width: Int, + /** Length of the SFPS sensor in pixels in current display density */ + val length: Int, /** * Whether the sensor is vertical when the device is in its default orientation (Rotation_0 or * Rotation_180) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 485e5122cfad98c9c302d223ad35fc116954271d..5c4ee3524a44d3611b5ceb35dddbd686256fb47c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -1,26 +1,44 @@ package com.android.systemui.communal.data.repository -import com.android.systemui.FeatureFlags +import com.android.systemui.Flags.communalHub +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** Encapsulates the state of communal mode. */ interface CommunalRepository { /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean + + /** + * Target scene as requested by the underlying [SceneTransitionLayout] or through + * [setDesiredScene]. + */ + val desiredScene: StateFlow<CommunalSceneKey> + + /** Updates the requested scene. */ + fun setDesiredScene(desiredScene: CommunalSceneKey) } @SysUISingleton class CommunalRepositoryImpl @Inject constructor( - private val featureFlags: FeatureFlags, private val featureFlagsClassic: FeatureFlagsClassic, ) : CommunalRepository { override val isCommunalEnabled: Boolean - get() = - featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && - featureFlags.communalHub() + get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub() + + private val _desiredScene: MutableStateFlow<CommunalSceneKey> = + MutableStateFlow(CommunalSceneKey.Blank) + override val desiredScene: StateFlow<CommunalSceneKey> = _desiredScene.asStateFlow() + + override fun setDesiredScene(desiredScene: CommunalSceneKey) { + _desiredScene.value = desiredScene + } } 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 62387079ab353c6da98f92e5141da5e23728ec88..ccccbb67c6c0c5b0fbb8601c740e6a14feb2cc66 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 @@ -19,10 +19,13 @@ package com.android.systemui.communal.domain.interactor import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map /** Encapsulates business-logic related to communal mode. */ @SysUISingleton @@ -47,4 +50,22 @@ constructor( * (have an allocated id). */ val widgetContent: Flow<List<CommunalWidgetContentModel>> = widgetRepository.communalWidgets + + /** + * Target scene as requested by the underlying [SceneTransitionLayout] or through + * [onSceneChanged]. + */ + val desiredScene: StateFlow<CommunalSceneKey> = communalRepository.desiredScene + + /** + * Flow that emits a boolean if the communal UI is showing, ie. the [desiredScene] is the + * [CommunalSceneKey.Communal]. + */ + val isCommunalShowing: Flow<Boolean> = + communalRepository.desiredScene.map { it == CommunalSceneKey.Communal } + + /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ + fun onSceneChanged(newScene: CommunalSceneKey) { + communalRepository.setDesiredScene(newScene) + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 276df4eb68ec49e3e29ef469524b4895f882b9c7..7f43eb5682d653ca69bc832c7f31a87dbf00969a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -19,12 +19,23 @@ package com.android.systemui.communal.domain.interactor import android.provider.Settings import com.android.systemui.communal.data.repository.CommunalTutorialRepository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.scene.shared.model.SceneKey import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch /** Encapsulates business-logic related to communal tutorial state. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -32,8 +43,12 @@ import kotlinx.coroutines.flow.distinctUntilChanged class CommunalTutorialInteractor @Inject constructor( - communalTutorialRepository: CommunalTutorialRepository, + @Application private val scope: CoroutineScope, + private val communalTutorialRepository: CommunalTutorialRepository, keyguardInteractor: KeyguardInteractor, + private val communalInteractor: CommunalInteractor, + private val sceneContainerFlags: SceneContainerFlags, + private val sceneInteractor: SceneInteractor, ) { /** An observable for whether the tutorial is available. */ val isTutorialAvailable: Flow<Boolean> = @@ -45,4 +60,63 @@ constructor( tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } .distinctUntilChanged() + + /** + * A flow of the new tutorial state after transitioning. The new state will be calculated based + * on the current tutorial state and transition state as following: + * HUB_MODE_TUTORIAL_NOT_STARTED + communal scene -> HUB_MODE_TUTORIAL_STARTED + * HUB_MODE_TUTORIAL_STARTED + non-communal scene -> HUB_MODE_TUTORIAL_COMPLETED + * HUB_MODE_TUTORIAL_COMPLETED + any scene -> won't emit + */ + private val tutorialStateToUpdate: Flow<Int> = + communalTutorialRepository.tutorialSettingState + .flatMapLatest { tutorialSettingState -> + if (tutorialSettingState == Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) { + return@flatMapLatest flowOf(null) + } + if (sceneContainerFlags.isEnabled()) { + sceneInteractor.desiredScene.map { sceneModel -> + nextStateAfterTransition( + tutorialSettingState, + sceneModel.key == SceneKey.Communal + ) + } + } else { + communalInteractor.isCommunalShowing.map { + nextStateAfterTransition(tutorialSettingState, it) + } + } + } + .filterNotNull() + .distinctUntilChanged() + + private fun nextStateAfterTransition(tutorialState: Int, isCommunalShowing: Boolean): Int? { + if (tutorialState == Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED && isCommunalShowing) { + return Settings.Secure.HUB_MODE_TUTORIAL_STARTED + } + if (tutorialState == Settings.Secure.HUB_MODE_TUTORIAL_STARTED && !isCommunalShowing) { + return Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED + } + return null + } + + private var job: Job? = null + private fun listenForTransitionToUpdateTutorialState() { + if (!communalInteractor.isCommunalEnabled) { + return + } + job = + scope.launch { + tutorialStateToUpdate.collect { + communalTutorialRepository.setTutorialState(it) + if (it == Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) { + job?.cancel() + } + } + } + } + + init { + listenForTransitionToUpdateTutorialState() + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt new file mode 100644 index 0000000000000000000000000000000000000000..2be909c8e6d017703aff3093a8cba708c092b9a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalSceneKey.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model + +/** Definition of the possible scenes for the communal UI. */ +sealed class CommunalSceneKey( + private val loggingName: String, +) { + /** The communal scene containing the hub UI. */ + object Communal : CommunalSceneKey("communal") + + /** The default scene, shows nothing and is only there to allow swiping to communal. */ + object Blank : CommunalSceneKey("blank") + + override fun toString(): String { + return loggingName + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt index dab6819e10281d04b6285479eaaaf4bfa4318fcb..4dfc371aaeefb53dcd65113e3b3e9d355235fd45 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt @@ -44,6 +44,8 @@ object CommunalTutorialIndicatorViewBinder { ) } } + + launch { viewModel.alpha.collect { view.alpha = it } } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt index eaf95508cf12b44219245520e06e61e5a80aac55..274e61a7499f03f9304091490f8ab7109e2a0b80 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt @@ -17,15 +17,21 @@ package com.android.systemui.communal.ui.viewmodel import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged /** View model for communal tutorial indicator on keyguard */ class CommunalTutorialIndicatorViewModel @Inject constructor( communalTutorialInteractor: CommunalTutorialInteractor, + bottomAreaInteractor: KeyguardBottomAreaInteractor, ) { /** An observable for whether the tutorial indicator view should be visible. */ val showIndicator: Flow<Boolean> = communalTutorialInteractor.isTutorialAvailable + + /** An observable for the alpha level for the tutorial indicator. */ + val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 390b580bad28b2b28d40783097a0f71ff5b64844..de9b56364c2420447635a04467bffc1a834e51fd 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -20,11 +20,13 @@ import android.appwidget.AppWidgetHost import android.content.Context import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @SysUISingleton @@ -33,7 +35,7 @@ class CommunalViewModel constructor( @Application private val context: Context, private val appWidgetHost: AppWidgetHost, - communalInteractor: CommunalInteractor, + private val communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, ) { /** Whether communal hub should show tutorial content. */ @@ -54,4 +56,9 @@ constructor( ) } } + + val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + fun onSceneChanged(scene: CommunalSceneKey) { + communalInteractor.onSceneChanged(scene) + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7915088b4642097bbb9ee7f560d19ac5cd9749f7..f3353c7f26dc3ef36d43317d6afd135c0d7744f1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -29,7 +29,6 @@ import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; -import com.android.systemui.aconfig.AConfigModule; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; import com.android.systemui.authentication.AuthenticationModule; @@ -163,7 +162,6 @@ import javax.inject.Named; @Module(includes = { AccessibilityModule.class, AccessibilityRepositoryModule.class, - AConfigModule.class, AppOpsModule.class, AssistModule.class, AuthenticationModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt index 9e54b5cba2763f7a49d5ff5c63988317631c9198..26c5ea6e164d626725a4c0748b4be8be103c4340 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt @@ -23,9 +23,9 @@ import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED import android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED import android.os.Handler -import android.os.Trace import android.util.Log import android.view.Display +import com.android.app.tracing.FlowTracing.traceEach import com.android.app.tracing.traceSection import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -43,10 +43,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -97,7 +96,6 @@ constructor( @Application applicationScope: CoroutineScope, @Background backgroundCoroutineDispatcher: CoroutineDispatcher ) : DisplayRepository { - // Displays are enabled only after receiving them in [onDisplayAdded] private val allDisplayEvents: Flow<DisplayEvent> = conflatedCallbackFlow { val callback = @@ -124,16 +122,22 @@ constructor( awaitClose { displayManager.unregisterDisplayListener(callback) } } .onStart { emit(DisplayEvent.Changed(Display.DEFAULT_DISPLAY)) } + .debugLog("allDisplayEvents") .flowOn(backgroundCoroutineDispatcher) override val displayChangeEvent: Flow<Int> = - allDisplayEvents.filter { it is DisplayEvent.Changed }.map { it.displayId } + allDisplayEvents.filterIsInstance<DisplayEvent.Changed>().map { event -> event.displayId } override val displayAdditionEvent: Flow<Display?> = - allDisplayEvents - .filter { it is DisplayEvent.Added } - .map { displayManager.getDisplay(it.displayId) } + allDisplayEvents.filterIsInstance<DisplayEvent.Added>().map { + displayManager.getDisplay(it.displayId) + } + /** + * Represents displays that went though the [DisplayListener.onDisplayAdded] callback. + * + * Those are commonly the ones provided by [DisplayManager.getDisplays] by default. + */ private val enabledDisplays = allDisplayEvents .map { getDisplays() } @@ -143,9 +147,7 @@ constructor( override val displays: Flow<Set<Display>> = enabledDisplays private fun getDisplays(): Set<Display> = - traceSection("DisplayRepository#getDisplays()") { - displayManager.displays?.toSet() ?: emptySet() - } + traceSection("$TAG#getDisplays()") { displayManager.displays?.toSet() ?: emptySet() } /** Propagate to the listeners only enabled displays */ private val enabledDisplayIds: Flow<Set<Int>> = @@ -153,7 +155,8 @@ constructor( .map { enabledDisplaysSet -> enabledDisplaysSet.map { it.displayId }.toSet() } .debugLog("enabledDisplayIds") - private val ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) + private val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet()) + private val ignoredDisplayIds: Flow<Set<Int>> = _ignoredDisplayIds.debugLog("ignoredDisplayIds") private fun getInitialConnectedDisplays(): Set<Int> = displayManager @@ -177,7 +180,7 @@ constructor( Log.d(TAG, "display with id=$id connected.") } connectedIds += id - ignoredDisplayIds.value -= id + _ignoredDisplayIds.value -= id trySend(connectedIds.toSet()) } @@ -186,7 +189,7 @@ constructor( if (DEBUG) { Log.d(TAG, "display with id=$id disconnected.") } - ignoredDisplayIds.value -= id + _ignoredDisplayIds.value -= id trySend(connectedIds.toSet()) } } @@ -214,17 +217,23 @@ constructor( private val connectedExternalDisplayIds: Flow<Set<Int>> = connectedDisplayIds .map { connectedDisplayIds -> - connectedDisplayIds - .filter { id -> displayManager.getDisplay(id)?.type == Display.TYPE_EXTERNAL } - .toSet() + traceSection("$TAG#filteringExternalDisplays") { + connectedDisplayIds + .filter { id -> getDisplayType(id) == Display.TYPE_EXTERNAL } + .toSet() + } } .flowOn(backgroundCoroutineDispatcher) .debugLog("connectedExternalDisplayIds") + private fun getDisplayType(displayId: Int): Int? = + traceSection("$TAG#getDisplayType") { displayManager.getDisplay(displayId)?.type } + /** - * Pending displays are the ones connected, but not enabled and not ignored. A connected display - * is ignored after the user makes the decision to use it or not. For now, the initial decision - * from the user is final and not reversible. + * Pending displays are the ones connected, but not enabled and not ignored. + * + * A connected display is ignored after the user makes the decision to use it or not. For now, + * the initial decision from the user is final and not reversible. */ private val pendingDisplayIds: Flow<Set<Int>> = combine(enabledDisplayIds, connectedExternalDisplayIds, ignoredDisplayIds) { @@ -241,12 +250,16 @@ constructor( } connectedExternalDisplayIds - enabledDisplaysIds - ignoredDisplayIds } - .debugLog("pendingDisplayIds") + .debugLog("allPendingDisplayIds") + + /** Which display id should be enabled among the pending ones. */ + private val pendingDisplayId: Flow<Int?> = + pendingDisplayIds.map { it.maxOrNull() }.distinctUntilChanged().debugLog("pendingDisplayId") override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> = - pendingDisplayIds - .map { pendingDisplayIds -> - val id = pendingDisplayIds.maxOrNull() ?: return@map null + pendingDisplayId + .map { displayId -> + val id = displayId ?: return@map null object : DisplayRepository.PendingDisplay { override val id = id @@ -263,7 +276,7 @@ constructor( override suspend fun ignore() { traceSection("DisplayRepository#ignore($id)") { - ignoredDisplayIds.value += id + _ignoredDisplayIds.value += id } } @@ -282,11 +295,7 @@ constructor( private fun <T> Flow<T>.debugLog(flowName: String): Flow<T> { return if (DEBUG) { - this.onEach { - Log.d(TAG, "$flowName: $it") - Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, "$TAG#$flowName", 0) - Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "$TAG#$flowName", "$it", 0) - } + traceEach(flowName, logcat = true) } else { this } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsClassicDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsClassicDebug.java index 36d789ec049bf635ed7a0c43b6c10c29891c29f1..87c12b4a5a59e4270bbaf76b20988ecf73ac89e4 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsClassicDebug.java +++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsClassicDebug.java @@ -17,6 +17,7 @@ package com.android.systemui.flags; import static com.android.systemui.Flags.exampleFlag; +import static com.android.systemui.Flags.sysuiTeamfood; import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS; import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG; import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS; @@ -38,13 +39,10 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.systemui.FeatureFlags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.util.settings.GlobalSettings; -import org.jetbrains.annotations.NotNull; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.Map; @@ -83,7 +81,6 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { private final Map<String, Boolean> mBooleanFlagCache = new ConcurrentHashMap<>(); private final Map<String, String> mStringFlagCache = new ConcurrentHashMap<>(); private final Map<String, Integer> mIntFlagCache = new ConcurrentHashMap<>(); - private final FeatureFlags mGantryFlags; private final Restarter mRestarter; private final ServerFlagReader.ChangeListener mOnPropertiesChanged = @@ -128,7 +125,6 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { @Main Resources resources, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags, - FeatureFlags gantryFlags, Restarter restarter) { mFlagManager = flagManager; mContext = context; @@ -137,7 +133,6 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { mSystemProperties = systemProperties; mServerFlagReader = serverFlagReader; mAllFlags = allFlags; - mGantryFlags = gantryFlags; mRestarter = restarter; } @@ -155,16 +150,16 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { } @Override - public boolean isEnabled(@NotNull UnreleasedFlag flag) { + public boolean isEnabled(@NonNull UnreleasedFlag flag) { return isEnabledInternal(flag); } @Override - public boolean isEnabled(@NotNull ReleasedFlag flag) { + public boolean isEnabled(@NonNull ReleasedFlag flag) { return isEnabledInternal(flag); } - private boolean isEnabledInternal(@NotNull BooleanFlag flag) { + private boolean isEnabledInternal(@NonNull BooleanFlag flag) { String name = flag.getName(); Boolean value = mBooleanFlagCache.get(name); @@ -266,7 +261,7 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { && !defaultValue && result == null && flag.getTeamfood()) { - return mGantryFlags.sysuiTeamfood(); + return sysuiTeamfood(); } return result == null ? mServerFlagReader.readServerOverride( @@ -539,7 +534,7 @@ public class FeatureFlagsClassicDebug implements FeatureFlagsClassic { @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("can override: true"); - pw.println("teamfood: " + mGantryFlags.sysuiTeamfood()); + pw.println("teamfood: " + sysuiTeamfood()); pw.println("booleans: " + mBooleanFlagCache.size()); pw.println("example_flag: " + exampleFlag()); pw.println("example_shared_flag: " + exampleSharedFlag()); diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index fa9866163646656e95e506378bade67cd9a5ef27..49b8ee632d7e71544d141af78a1f18b5178573a0 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -258,7 +258,7 @@ object Flags { // TODO(b/290652751): Tracking bug. @JvmField val MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA = - unreleasedFlag("migrate_split_keyguard_bottom_area", teamfood = true) + unreleasedFlag("migrate_split_keyguard_bottom_area") // TODO(b/297037052): Tracking bug. @JvmField @@ -273,7 +273,7 @@ object Flags { /** Migrate the lock icon view to the new keyguard root view. */ // TODO(b/286552209): Tracking bug. - @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag("migrate_lock_icon", teamfood = true) + @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag("migrate_lock_icon") // TODO(b/288276738): Tracking bug. @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag("widget_on_keyguard") @@ -296,11 +296,6 @@ object Flags { @JvmField val MIGRATE_CLOCKS_TO_BLUEPRINT = unreleasedFlag("migrate_clocks_to_blueprint") - /** Migrate KeyguardRootView to use composables. */ - // TODO(b/301969856): Tracking Bug. - @JvmField val KEYGUARD_ROOT_VIEW_USE_COMPOSE = - unreleasedFlag("keyguard_root_view_use_compose") - /** Enables preview loading animation in the wallpaper picker. */ // TODO(b/274443705): Tracking Bug @JvmField @@ -532,10 +527,9 @@ object Flags { @Keep @JvmField val WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES = - unreleasedFlag( - name = "screen_record_enterprise_policies", + releasedFlag( + name = "enable_screen_record_enterprise_policies", namespace = DeviceConfig.NAMESPACE_WINDOW_MANAGER, - teamfood = false ) // TODO(b/293252410) : Tracking Bug diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt index 1acea5cfc579db0054ff04dd945fd336c5e45656..0bee48a63a284a958c934a973518af04eb5d5a32 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/SideFpsProgressBarViewBinder.kt @@ -16,19 +16,30 @@ package com.android.systemui.keyguard.ui.binder +import android.animation.ValueAnimator +import android.graphics.Point import com.android.systemui.CoreStartable import com.android.systemui.biometrics.SideFpsController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.view.SideFpsProgressBar import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel +import com.android.systemui.log.SideFpsLogger +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.kotlin.Quint +import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +private const val spfsProgressBarCommand = "sfps-progress-bar" + @SysUISingleton class SideFpsProgressBarViewBinder @Inject @@ -37,38 +48,124 @@ constructor( private val view: SideFpsProgressBar, @Application private val applicationScope: CoroutineScope, private val sfpsController: dagger.Lazy<SideFpsController>, + private val logger: SideFpsLogger, + private val commandRegistry: CommandRegistry, + private val featureFlagsClassic: FeatureFlagsClassic, ) : CoreStartable { override fun start() { + if (!featureFlagsClassic.isEnabled(Flags.REST_TO_UNLOCK)) { + return + } + // When the rest to unlock feature is disabled by the user, stop any coroutines that are + // not required. + var layoutJob: Job? = null + var progressJob: Job? = null + commandRegistry.registerCommand(spfsProgressBarCommand) { SfpsProgressBarCommand() } applicationScope.launch { viewModel.isProlongedTouchRequiredForAuthentication.collectLatest { enabled -> + logger.isProlongedTouchRequiredForAuthenticationChanged(enabled) if (enabled) { - launch { + layoutJob = launch { combine( viewModel.isVisible, - viewModel.sensorLocation, - viewModel.shouldRotate90Degrees, + viewModel.progressBarLocation, + viewModel.rotation, viewModel.isFingerprintAuthRunning, - viewModel.sensorWidth, + viewModel.progressBarLength, ::Quint ) - .collectLatest { - (visible, location, shouldRotate, fpDetectRunning, sensorWidth) -> - view.updateView(visible, location, shouldRotate, sensorWidth) - // We have to hide the SFPS indicator as the progress bar will - // be shown at the same location - if (visible) { - sfpsController.get().hideIndicator() - } else if (fpDetectRunning) { - sfpsController.get().showIndicator() - } + .collectLatest { (visible, location, rotation, fpDetectRunning, length) + -> + updateView( + visible, + location, + fpDetectRunning, + length, + viewModel.progressBarThickness, + rotation, + ) } } - launch { viewModel.progress.collectLatest { view.setProgress(it) } } + progressJob = launch { + viewModel.progress.collectLatest { view.setProgress(it) } + } } else { - view.hideOverlay() + view.hide() + layoutJob?.cancel() + progressJob?.cancel() + } + } + } + } + + private fun updateView( + visible: Boolean, + location: Point, + fpDetectRunning: Boolean, + length: Int, + thickness: Int, + rotation: Float, + ) { + logger.sfpsProgressBarStateChanged(visible, location, fpDetectRunning, length, rotation) + view.updateView(visible, location, length, thickness, rotation) + // We have to hide the SFPS indicator as the progress bar will + // be shown at the same location + if (visible) { + logger.hidingSfpsIndicator() + sfpsController.get().hideIndicator() + } else if (fpDetectRunning) { + logger.showingSfpsIndicator() + sfpsController.get().showIndicator() + } + } + + inner class SfpsProgressBarCommand : Command { + private var animator: ValueAnimator? = null + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty() || args[0] == "show" && args.size != 6) { + pw.println("invalid command") + help(pw) + } else { + when (args[0]) { + "show" -> { + animator?.cancel() + updateView( + visible = true, + location = Point(Integer.parseInt(args[1]), Integer.parseInt(args[2])), + fpDetectRunning = true, + length = Integer.parseInt(args[3]), + thickness = Integer.parseInt(args[4]), + rotation = Integer.parseInt(args[5]).toFloat(), + ) + animator = + ValueAnimator.ofFloat(0.0f, 1.0f).apply { + repeatMode = ValueAnimator.REVERSE + repeatCount = ValueAnimator.INFINITE + addUpdateListener { view.setProgress(it.animatedValue as Float) } + } + animator?.start() + } + "hide" -> { + animator?.cancel() + updateView( + visible = false, + location = Point(0, 0), + fpDetectRunning = false, + length = 0, + thickness = 0, + rotation = 0.0f, + ) + } } } } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $spfsProgressBarCommand <command>") + pw.println("Available commands:") + pw.println(" show x y width height rotation") + pw.println(" hide") + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt index f7ab1ee7758255cc7a56c6173bf00e43d06436d6..853f1769994e2ca6fb350ef0a1f22341896c1af0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/SideFpsProgressBar.kt @@ -22,17 +22,16 @@ import android.graphics.Point import android.view.Gravity import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.WindowManager import android.widget.ProgressBar -import com.android.systemui.biometrics.Utils +import androidx.core.view.isGone import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R import javax.inject.Inject private const val TAG = "SideFpsProgressBar" -const val progressBarHeight = 100 - @SysUISingleton class SideFpsProgressBar @Inject @@ -40,31 +39,36 @@ constructor( private val layoutInflater: LayoutInflater, private val windowManager: WindowManager, ) { - private var progressBarWidth = 200 + private var overlayView: View? = null + fun updateView( visible: Boolean, - location: Point, - shouldRotate90Degrees: Boolean, - progressBarWidth: Int + viewLeftTopLocation: Point, + progressBarWidth: Int, + progressBarHeight: Int, + rotation: Float, ) { if (visible) { - this.progressBarWidth = progressBarWidth - createAndShowOverlay(location, shouldRotate90Degrees) + createAndShowOverlay(viewLeftTopLocation, rotation, progressBarWidth, progressBarHeight) } else { - hideOverlay() + hide() } } - fun hideOverlay() { - overlayView = null + fun hide() { + progressBar?.isGone = true } private val overlayViewParams = WindowManager.LayoutParams( - progressBarHeight, - progressBarWidth, + // overlay is always full screen + MATCH_PARENT, + MATCH_PARENT, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, - Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, PixelFormat.TRANSPARENT ) .apply { @@ -78,37 +82,31 @@ constructor( WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION } - private var overlayView: View? = null - set(value) { - field?.let { oldView -> windowManager.removeView(oldView) } - field = value - field?.let { newView -> windowManager.addView(newView, overlayViewParams) } - } - private fun createAndShowOverlay( - fingerprintSensorLocation: Point, - shouldRotate90Degrees: Boolean + viewLeftTop: Point, + rotation: Float, + progressBarLength: Int, + progressBarThickness: Int, ) { if (overlayView == null) { overlayView = layoutInflater.inflate(R.layout.sidefps_progress_bar, null, false) + windowManager.addView(overlayView, overlayViewParams) + progressBar?.pivotX = 0.0f + progressBar?.pivotY = 0.0f } - overlayViewParams.x = fingerprintSensorLocation.x - overlayViewParams.y = fingerprintSensorLocation.y - if (shouldRotate90Degrees) { - overlayView?.rotation = 270.0f - overlayViewParams.width = progressBarHeight - overlayViewParams.height = progressBarWidth - } else { - overlayView?.rotation = 0.0f - overlayViewParams.width = progressBarWidth - overlayViewParams.height = progressBarHeight - } - windowManager.updateViewLayout(overlayView, overlayViewParams) + progressBar?.layoutParams?.width = progressBarLength + progressBar?.layoutParams?.height = progressBarThickness + progressBar?.translationX = viewLeftTop.x.toFloat() + progressBar?.translationY = viewLeftTop.y.toFloat() + progressBar?.rotation = rotation + progressBar?.isGone = false + overlayView?.requestLayout() } fun setProgress(value: Float) { - overlayView - ?.findViewById<ProgressBar?>(R.id.side_fps_progress_bar) - ?.setProgress((value * 100).toInt(), false) + progressBar?.setProgress((value * 100).toInt(), false) } + + private val progressBar: ProgressBar? + get() = overlayView?.findViewById(R.id.side_fps_progress_bar) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index 2c3b431715a9df67fc31c8aa134ac3f467dc0768..a0f5baff29bae93c25f57e573ab5884bc6cad095 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -17,64 +17,137 @@ package com.android.systemui.keyguard.ui.viewmodel import android.animation.ValueAnimator +import android.content.Context import android.graphics.Point import androidx.core.animation.doOnEnd import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor +import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @SysUISingleton class SideFpsProgressBarViewModel @Inject constructor( + private val context: Context, private val fpAuthRepository: DeviceEntryFingerprintAuthRepository, private val sfpsSensorInteractor: SideFpsSensorInteractor, displayStateInteractor: DisplayStateInteractor, @Application private val applicationScope: CoroutineScope, + private val featureFlagsClassic: FeatureFlagsClassic, ) { private val _progress = MutableStateFlow(0.0f) private val _visible = MutableStateFlow(false) private var _animator: ValueAnimator? = null + private var animatorJob: Job? = null private fun onFingerprintCaptureCompleted() { _visible.value = false _progress.value = 0.0f } + private val additionalSensorLengthPadding = + context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt() + val isVisible: Flow<Boolean> = _visible.asStateFlow() val progress: Flow<Float> = _progress.asStateFlow() - val sensorWidth: Flow<Int> = sfpsSensorInteractor.sensorLocation.map { it.width } + val progressBarLength: Flow<Int> = + sfpsSensorInteractor.sensorLocation + .map { it.length + additionalSensorLengthPadding } + .distinctUntilChanged() + + val progressBarThickness = + context.resources.getDimension(R.dimen.sfps_progress_bar_thickness).toInt() + + val progressBarLocation = + combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) + .map { (rotation, sensorLocation) -> + val paddingFromEdge = + context.resources + .getDimension(R.dimen.sfps_progress_bar_padding_from_edge) + .toInt() + val lengthOfTheProgressBar = sensorLocation.length + additionalSensorLengthPadding + val viewLeftTop = Point(sensorLocation.left, sensorLocation.top) + val totalDistanceFromTheEdge = paddingFromEdge + progressBarThickness - val sensorLocation: Flow<Point> = - sfpsSensorInteractor.sensorLocation.map { Point(it.left, it.top) } + val isSensorVerticalNow = + sensorLocation.isSensorVerticalInDefaultOrientation == + rotation.isDefaultOrientation() + if (isSensorVerticalNow) { + // Sensor is vertical to the current orientation, we rotate it 270 deg + // around the (left,top) point as the pivot. We need to push it down the + // length of the progress bar so that it is still aligned to the sensor + viewLeftTop.y += lengthOfTheProgressBar + val isSensorOnTheNearEdge = + rotation == DisplayRotation.ROTATION_180 || + rotation == DisplayRotation.ROTATION_90 + if (isSensorOnTheNearEdge) { + // Add just the padding from the edge to push the progress bar right + viewLeftTop.x += paddingFromEdge + } else { + // View left top is pushed left from the edge by the progress bar thickness + // and the padding. + viewLeftTop.x -= totalDistanceFromTheEdge + } + } else { + // Sensor is horizontal to the current orientation. + val isSensorOnTheNearEdge = + rotation == DisplayRotation.ROTATION_0 || + rotation == DisplayRotation.ROTATION_90 + if (isSensorOnTheNearEdge) { + // Add just the padding from the edge to push the progress bar down + viewLeftTop.y += paddingFromEdge + } else { + // Sensor is now at the bottom edge of the device in the current rotation. + // We want to push it up from the bottom edge by the padding and + // the thickness of the progressbar. + viewLeftTop.y -= totalDistanceFromTheEdge + viewLeftTop.x -= additionalSensorLengthPadding + } + } + viewLeftTop + } val isFingerprintAuthRunning: Flow<Boolean> = fpAuthRepository.isRunning - val shouldRotate90Degrees: Flow<Boolean> = + val rotation: Flow<Float> = combine(displayStateInteractor.currentRotation, sfpsSensorInteractor.sensorLocation, ::Pair) .map { (rotation, sensorLocation) -> - if (rotation.isDefaultOrientation()) { - sensorLocation.isSensorVerticalInDefaultOrientation + if ( + rotation.isDefaultOrientation() == + sensorLocation.isSensorVerticalInDefaultOrientation + ) { + // We should rotate the progress bar 270 degrees in the clockwise direction with + // the left top point as the pivot so that it fills up from bottom to top + 270.0f } else { - !sensorLocation.isSensorVerticalInDefaultOrientation + 0.0f } } @@ -82,26 +155,32 @@ constructor( sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication init { - applicationScope.launch { - combine( - sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication, - sfpsSensorInteractor.authenticationDuration, - ::Pair - ) - .collectLatest { (enabled, authDuration) -> - if (!enabled) return@collectLatest + if (featureFlagsClassic.isEnabled(Flags.REST_TO_UNLOCK)) { + launchAnimator() + } + } - launch { - fpAuthRepository.authenticationStatus.collectLatest { authStatus -> + private fun launchAnimator() { + applicationScope.launch { + sfpsSensorInteractor.isProlongedTouchRequiredForAuthentication.collectLatest { enabled + -> + if (!enabled) { + animatorJob?.cancel() + return@collectLatest + } + animatorJob = + fpAuthRepository.authenticationStatus + .onEach { authStatus -> when (authStatus) { is AcquiredFingerprintAuthenticationStatus -> { if (authStatus.fingerprintCaptureStarted) { - _visible.value = true _animator?.cancel() _animator = ValueAnimator.ofFloat(0.0f, 1.0f) - .setDuration(authDuration) + .setDuration( + sfpsSensorInteractor.authenticationDuration + ) .apply { addUpdateListener { _progress.value = it.animatedValue as Float @@ -131,8 +210,9 @@ constructor( else -> Unit } } - } - } + .onCompletion { _animator?.cancel() } + .launchIn(applicationScope) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt index 74923eec48913eccdc0c7c1fad35663ef914725e..919072a6322041bd80a5ee251db838c63ed35e09 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt @@ -17,8 +17,6 @@ package com.android.systemui.log import android.graphics.Point -import android.graphics.Rect -import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.core.LogLevel import com.android.systemui.log.dagger.BouncerLog @@ -40,9 +38,9 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe fun sfpsProgressBarStateChanged( visible: Boolean, location: Point, - shouldRotate: Boolean, fpDetectRunning: Boolean, - sensorWidth: Int + sensorWidth: Int, + rotation: Float, ) { buffer.log( TAG, @@ -51,14 +49,14 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe bool1 = visible int1 = location.x int2 = location.y - bool2 = shouldRotate + str1 = "$rotation" bool3 = fpDetectRunning long1 = sensorWidth.toLong() }, { "SFPS progress bar state changed: visible: $bool1, " + "sensorLocation (x, y): ($int1, $int2), " + - "shouldRotate = $bool2, " + + "rotation = $str1, " + "fpDetectRunning: $bool3, " + "sensorWidth: $long1" } @@ -87,44 +85,25 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe ) } - fun logStateChange(sfpsAvailable: Boolean, settingEnabled: Boolean) { - buffer.log( - TAG, - LogLevel.DEBUG, - { - bool1 = sfpsAvailable - bool2 = settingEnabled - }, - { "SFPS rest to unlock state changed: sfpsAvailable: $bool1, settingEnabled: $bool2" } - ) - } - fun sensorLocationStateChanged( - windowSize: Rect?, - rotation: DisplayRotation, - displayWidth: Int, - displayHeight: Int, - sensorWidth: Int, - sensorVerticalInDefaultOrientation: Boolean + pointOnScreenX: Int, + pointOnScreenY: Int, + sensorLength: Int, + isSensorVerticalInDefaultOrientation: Boolean ) { buffer.log( TAG, LogLevel.DEBUG, { - str1 = "$windowSize" - str2 = rotation.name - int1 = displayWidth - int2 = displayHeight - long1 = sensorWidth.toLong() - bool1 = sensorVerticalInDefaultOrientation + int1 = pointOnScreenX + int2 = pointOnScreenY + str2 = "$sensorLength" + bool1 = isSensorVerticalInDefaultOrientation }, { - "sensorLocation state changed: " + - "windowSize: $str1, " + - "rotation: $str2, " + - "widthInRotation0: $int1, " + - "heightInRotation0: $int2, " + - "sensorWidth: $long1, " + + "SideFpsSensorLocation state changed: " + + "pointOnScreen: ($int1, $int2), " + + "sensorLength: $str2, " + "sensorVerticalInDefaultOrientation: $bool1" } ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index d9ff36f2875e7cd25d9ab30721922208e7700bdc..b1ff708d020b5227074c3cc2cb58143df9d4f1e8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -36,7 +36,9 @@ import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators import com.android.app.tracing.traceSection import com.android.keyguard.KeyguardViewController +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle @@ -57,6 +59,8 @@ import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch private val TAG: String = MediaHierarchyManager::class.java.simpleName @@ -96,11 +100,13 @@ constructor( private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, + private val communalInteractor: CommunalInteractor, configurationController: ConfigurationController, wakefulnessLifecycle: WakefulnessLifecycle, panelEventsEvents: ShadeStateEvents, private val secureSettings: SecureSettings, @Main private val handler: Handler, + @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, ) { @@ -209,7 +215,7 @@ constructor( else result.setIntersect(animationStartClipping, targetClipping) } - private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1) + private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_COMMUNAL_HUB + 1) /** * The last location where this view was at before going to the desired location. This is useful * for guided transitions. @@ -401,6 +407,9 @@ constructor( } } + /** Is the communal UI showing */ + private var isCommunalShowing: Boolean = false + /** * The current cross fade progress. 0.5f means it's just switching between the start and the end * location and the content is fully faded, while 0.75f means that we're halfway faded in again @@ -563,6 +572,14 @@ constructor( settingsObserver, UserHandle.USER_ALL ) + + // Listen to the communal UI state. + coroutineScope.launch { + communalInteractor.isCommunalShowing.collect { value -> + isCommunalShowing = value + updateDesiredLocation(forceNoAnimation = true) + } + } } private fun updateConfiguration() { @@ -1115,6 +1132,9 @@ constructor( qsExpansion > 0.4f && onLockscreen -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS + // TODO(b/308813166): revisit logic once interactions between the hub and + // shade/keyguard state are finalized + isCommunalShowing && communalInteractor.isCommunalEnabled -> LOCATION_COMMUNAL_HUB onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN else -> LOCATION_QQS } @@ -1224,6 +1244,9 @@ constructor( /** Attached on the dream overlay */ const val LOCATION_DREAM_OVERLAY = 3 + /** Attached to a view in the communal UI grid */ + const val LOCATION_COMMUNAL_HUB = 4 + /** Attached at the root of the hierarchy in an overlay */ const val IN_OVERLAY = -1000 @@ -1261,7 +1284,8 @@ private annotation class TransformationType MediaHierarchyManager.LOCATION_QS, MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN, - MediaHierarchyManager.LOCATION_DREAM_OVERLAY + MediaHierarchyManager.LOCATION_DREAM_OVERLAY, + MediaHierarchyManager.LOCATION_COMMUNAL_HUB ] ) @Retention(AnnotationRetention.SOURCE) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index 20ea60fa3a5feafefdce438631551b86d4db6664..16a703a6bfdd52ed88d3ffb04ea64d9356202649 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -20,11 +20,11 @@ import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceIdSequence import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaLocation +import com.android.systemui.res.R import java.lang.IllegalArgumentException import javax.inject.Inject @@ -154,6 +154,8 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM + MediaHierarchyManager.LOCATION_COMMUNAL_HUB -> + MediaUiEvent.MEDIA_CAROUSEL_LOCATION_COMMUNAL else -> throw IllegalArgumentException("Unknown media carousel location $location") } logger.log(event) @@ -276,6 +278,8 @@ enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039), @UiEvent(doc = "The media carousel moved to the dream state") MEDIA_CAROUSEL_LOCATION_DREAM(1040), + @UiEvent(doc = "The media carousel moved to the communal hub UI") + MEDIA_CAROUSEL_LOCATION_COMMUNAL(1520), @UiEvent(doc = "A media recommendation card was added to the media carousel") MEDIA_RECOMMENDATION_ADDED(1041), @UiEvent(doc = "A media recommendation card was removed from the media carousel") diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 888cd0bf8b9aed7d5128629d613cc644b10c41ab..8f752e59e80690eac0b7cda299412b3a0bfe034f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -46,6 +46,7 @@ public interface MediaModule { String QUICK_QS_PANEL = "media_quick_qs_panel"; String KEYGUARD = "media_keyguard"; String DREAM = "dream"; + String COMMUNAL_HUB = "communal_Hub"; /** */ @Provides @@ -87,6 +88,16 @@ public interface MediaModule { return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); } + /** */ + @Provides + @SysUISingleton + @Named(COMMUNAL_HUB) + static MediaHost providesCommunalMediaHost(MediaHost.MediaHostStateHolder stateHolder, + MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, + MediaHostStatesManager statesManager) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + } + /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */ @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt index 97ec654a627b83b643483c548fc78ead15ddc2c7..6e3b7b865cb0ae81fd947dde6f7673fa9a07285c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt @@ -31,7 +31,7 @@ class MediaMuteAwaitConnectionManagerFactory @Inject constructor( private val logger: MediaMuteAwaitLogger, @Main private val mainExecutor: Executor ) { - private val deviceIconUtil = DeviceIconUtil() + private val deviceIconUtil = DeviceIconUtil(context) /** Creates a [MediaMuteAwaitConnectionManager]. */ fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d7d88fbfa79ec0cec86b2956e9b3afe3d9d9800 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt @@ -0,0 +1,61 @@ +/* + * 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.qs.tiles.impl.custom.data.repository + +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.qs.external.TileServiceManager +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundScope +import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileUser +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn + +interface CustomTilePackageUpdatesRepository { + + val packageChanges: Flow<Unit> +} + +@CustomTileBoundScope +class CustomTilePackageUpdatesRepositoryImpl +@Inject +constructor( + tileSpec: TileSpec.CustomTileSpec, + @CustomTileUser user: UserHandle, + serviceManager: TileServiceManager, + defaultsRepository: CustomTileDefaultsRepository, + @CustomTileBoundScope boundScope: CoroutineScope, +) : CustomTilePackageUpdatesRepository { + + override val packageChanges: Flow<Unit> = + ConflatedCallbackFlow.conflatedCallbackFlow { + serviceManager.setTileChangeListener { changedComponentName -> + if (changedComponentName == tileSpec.componentName) { + trySend(Unit) + } + } + + awaitClose { serviceManager.setTileChangeListener(null) } + } + .onEach { defaultsRepository.requestNewDefaults(user, tileSpec.componentName, true) } + .shareIn(boundScope, SharingStarted.WhileSubscribed()) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt index e33b3e9176296f9bcf6ce33729569bccb2bba3a7..d382d2063f066f1835da985fd819561f91733dca 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.CoroutineScope /** @see CustomTileBoundScope */ @CustomTileBoundScope -@Subcomponent +@Subcomponent(modules = [CustomTileBoundModule::class]) interface CustomTileBoundComponent { @Subcomponent.Builder diff --git a/packages/SystemUI/src/com/android/systemui/aconfig/AConfigModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt similarity index 58% rename from packages/SystemUI/src/com/android/systemui/aconfig/AConfigModule.kt rename to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt index fa61bba6922de00c50a05d369797196e754bfc1e..889424a8e8d352e5db7fe179408c63ff3ee89a42 100644 --- a/packages/SystemUI/src/com/android/systemui/aconfig/AConfigModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt @@ -14,22 +14,18 @@ * limitations under the License. */ -package com.android.systemui.aconfig +package com.android.systemui.qs.tiles.impl.custom.di.bound -import com.android.systemui.FeatureFlags -import com.android.systemui.FeatureFlagsImpl -import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl +import dagger.Binds import dagger.Module -import dagger.Provides @Module -abstract class AConfigModule { - @Module - companion object { - @Provides - @SysUISingleton - fun providesImpl(): FeatureFlags { - return FeatureFlagsImpl() - } - } +interface CustomTileBoundModule { + + @Binds + fun bindCustomTilePackageUpdatesRepository( + impl: CustomTilePackageUpdatesRepositoryImpl + ): CustomTilePackageUpdatesRepository } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 7353379b2e79321d264aa65dd70a878a581c6afb..b1440031a2a0afe7871a2037250d7946dafffb47 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -26,9 +26,12 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -105,6 +108,26 @@ constructor( initialValue = null, ) + /** + * Whether user input is ongoing for the current transition. For example, if the user is swiping + * their finger to transition between scenes, this value will be true while their finger is on + * the screen, then false for the rest of the transition. + */ + @OptIn(ExperimentalCoroutinesApi::class) + val isTransitionUserInputOngoing: StateFlow<Boolean> = + transitionState + .flatMapLatest { + when (it) { + is ObservableTransitionState.Transition -> it.isUserInputOngoing + is ObservableTransitionState.Idle -> flowOf(false) + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = repository.isVisible diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 9ba02b1aa9a86064a46d839ee60dc7d784342781..49bceefb24e0e8737cd4d2ab9c18aecec40d4f19 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -17,8 +17,8 @@ package com.android.systemui.scene.shared.flag import androidx.annotation.VisibleForTesting -import com.android.systemui.FeatureFlags import com.android.systemui.Flags as AConfigFlags +import com.android.systemui.Flags.sceneContainer import com.android.systemui.compose.ComposeFacade import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic @@ -50,7 +50,6 @@ class SceneContainerFlagsImpl @AssistedInject constructor( private val featureFlagsClassic: FeatureFlagsClassic, - featureFlags: FeatureFlags, @Assisted private val isComposeAvailable: Boolean, ) : SceneContainerFlags { @@ -72,7 +71,7 @@ constructor( listOf( AconfigFlagMustBeEnabled( flagName = AConfigFlags.FLAG_SCENE_CONTAINER, - flagValue = featureFlags.sceneContainer(), + flagValue = sceneContainer(), ), ) + classicFlagTokens.map { flagToken -> FlagMustBeEnabled(flagToken) } + diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/ObservableTransitionState.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/ObservableTransitionState.kt index 3927873f8ba8bbd754988d626e2ee953888f5c61..f704894e56e210a19989bfce6ba86744dd3c86f9 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/ObservableTransitionState.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/ObservableTransitionState.kt @@ -42,6 +42,13 @@ sealed class ObservableTransitionState { * scene, this value will remain true after the pointer is no longer touching the screen and * will be true in any transition created to animate back to the original position. */ - val isUserInputDriven: Boolean, + val isInitiatedByUserInput: Boolean, + + /** + * Whether user input is currently driving the transition. For example, if a user is + * dragging a pointer, this emits true. Once they lift their finger, this emits false while + * the transition completes/settles. + */ + val isUserInputOngoing: Flow<Boolean>, ) : ObservableTransitionState() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index a2627eddf05de1e896df0ed62e0092716f3c1140..a2ca49d9ba57325e61a82a06864481f9fa82b4a6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -113,6 +113,7 @@ public class NotificationShadeWindowViewController implements Dumpable { private final SysUIKeyEventHandler mSysUIKeyEventHandler; private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; private final AlternateBouncerInteractor mAlternateBouncerInteractor; + private final QuickSettingsController mQuickSettingsController; private GestureDetector mPulsingWakeupGestureHandler; private GestureDetector mDreamingWakeupGestureHandler; private View mBrightnessMirror; @@ -188,6 +189,7 @@ public class NotificationShadeWindowViewController implements Dumpable { BouncerMessageInteractor bouncerMessageInteractor, BouncerLogger bouncerLogger, SysUIKeyEventHandler sysUIKeyEventHandler, + QuickSettingsController quickSettingsController, PrimaryBouncerInteractor primaryBouncerInteractor, AlternateBouncerInteractor alternateBouncerInteractor, SelectedUserInteractor selectedUserInteractor) { @@ -220,6 +222,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mSysUIKeyEventHandler = sysUIKeyEventHandler; mPrimaryBouncerInteractor = primaryBouncerInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; + mQuickSettingsController = quickSettingsController; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -454,6 +457,16 @@ public class NotificationShadeWindowViewController implements Dumpable { && !bouncerShowing && !mStatusBarStateController.isDozing()) { if (mDragDownHelper.isDragDownEnabled()) { + if (mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { + // When on lockscreen, if the touch originates at the top of the screen + // go directly to QS and not the shade + if (mQuickSettingsController.shouldQuickSettingsIntercept( + ev.getX(), ev.getY(), 0)) { + mShadeLogger.d("NSWVC: QS intercepted"); + return true; + } + } + // This handles drag down over lockscreen boolean result = mDragDownHelper.onInterceptTouchEvent(ev); if (mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index a4c4503a6550c313a2f3f3cb4177355ff1edaeb3..b2ffeb3f29254f3c63a7cebda341fc3b1cb53ee4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -275,7 +275,7 @@ constructor( when (state) { is ObservableTransitionState.Idle -> false is ObservableTransitionState.Transition -> - state.isUserInputDriven && + state.isInitiatedByUserInput && (state.toScene == sceneKey || state.fromScene == sceneKey) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 8064f049c88e5722bf71f82eb3d3f3d359d53e93..12ee54d4977d03c836cd3684407f0e376294880d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key +import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel +import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -29,13 +32,64 @@ import kotlinx.coroutines.flow.MutableStateFlow */ @SysUISingleton class ActiveNotificationListRepository @Inject constructor() { - /** - * Notifications actively presented to the user in the notification stack. - * - * @see com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener - */ - val activeNotifications = MutableStateFlow(emptyMap<String, ActiveNotificationModel>()) + /** Notifications actively presented to the user in the notification list. */ + val activeNotifications = MutableStateFlow(ActiveNotificationsStore()) /** Are any already-seen notifications currently filtered out of the active list? */ val hasFilteredOutSeenNotifications = MutableStateFlow(false) } + +/** Represents the notification list, comprised of groups and individual notifications. */ +data class ActiveNotificationsStore( + /** Notification groups, stored by key. */ + val groups: Map<String, ActiveNotificationGroupModel> = emptyMap(), + /** All individual notifications, including top-level and group children, stored by key. */ + val individuals: Map<String, ActiveNotificationModel> = emptyMap(), + /** + * Ordered top-level list of entries in the notification list (either groups or individual), + * represented as [Key]s. The associated [ActiveNotificationEntryModel] can be retrieved by + * invoking [get]. + */ + val renderList: List<Key> = emptyList(), +) { + operator fun get(key: Key): ActiveNotificationEntryModel? { + return when (key) { + is Key.Group -> groups[key.key] + is Key.Individual -> individuals[key.key] + } + } + + /** Unique key identifying an [ActiveNotificationEntryModel] in the store. */ + sealed class Key { + data class Individual(val key: String) : Key() + data class Group(val key: String) : Key() + } + + /** Mutable builder for an [ActiveNotificationsStore]. */ + class Builder { + private val groups = mutableMapOf<String, ActiveNotificationGroupModel>() + private val individuals = mutableMapOf<String, ActiveNotificationModel>() + private val renderList = mutableListOf<Key>() + + fun build() = ActiveNotificationsStore(groups, individuals, renderList) + + fun addEntry(entry: ActiveNotificationEntryModel) { + when (entry) { + is ActiveNotificationModel -> addIndividualNotif(entry) + is ActiveNotificationGroupModel -> addNotifGroup(entry) + } + } + + fun addIndividualNotif(notif: ActiveNotificationModel) { + renderList.add(Key.Individual(notif.key)) + individuals[notif.key] = notif + } + + fun addNotifGroup(group: ActiveNotificationGroupModel) { + renderList.add(Key.Group(group.key)) + groups[group.key] = group + individuals[group.summary.key] = group.summary + group.children.forEach { individuals[it.key] = it } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index bfec60bcd6db49e7303de191383c3f355dfd0cbf..85ba205d3a0a31ebfca7962916cc93b713f90937 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -27,6 +28,16 @@ constructor( repository: ActiveNotificationListRepository, ) { /** Notifications actively presented to the user in the notification stack, in order. */ - val notifications: Flow<Collection<ActiveNotificationModel>> = - repository.activeNotifications.map { it.values } + val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> = + repository.activeNotifications.map { store -> + store.renderList.map { key -> + val entry = + store[key] + ?: error("Could not find notification with key $key in active notif store.") + when (entry) { + is ActiveNotificationGroupModel -> entry.summary + is ActiveNotificationModel -> entry + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 604ecbc047ff50cb0609739f1c0b13d2d3e864fb..6f4ed9db20b1b9d978cf7d81e1661fbe3f483864 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -16,15 +16,18 @@ package com.android.systemui.statusbar.notification.domain.interactor import android.graphics.drawable.Icon +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore +import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel +import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.flow.update -private typealias ModelStore = Map<String, ActiveNotificationModel> - /** * Logic for passing information from the * [com.android.systemui.statusbar.notification.collection.NotifPipeline] to the presentation @@ -37,106 +40,166 @@ constructor( private val sectionStyleProvider: SectionStyleProvider, ) { /** - * Sets the current list of rendered notification entries as displayed in the notification - * stack. - * - * @see com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository.activeNotifications + * Sets the current list of rendered notification entries as displayed in the notification list. */ fun setRenderedList(entries: List<ListEntry>) { repository.activeNotifications.update { existingModels -> - entries.associateBy( - keySelector = { it.key }, - valueTransform = { it.toModel(existingModels) }, - ) + buildActiveNotificationsStore(existingModels, sectionStyleProvider) { + entries.forEach(::addListEntry) + } } } +} + +private fun buildActiveNotificationsStore( + existingModels: ActiveNotificationsStore, + sectionStyleProvider: SectionStyleProvider, + block: ActiveNotificationsStoreBuilder.() -> Unit +): ActiveNotificationsStore = + ActiveNotificationsStoreBuilder(existingModels, sectionStyleProvider).apply(block).build() + +private class ActiveNotificationsStoreBuilder( + private val existingModels: ActiveNotificationsStore, + private val sectionStyleProvider: SectionStyleProvider, +) { + private val builder = ActiveNotificationsStore.Builder() + + fun build(): ActiveNotificationsStore = builder.build() - private fun ListEntry.toModel( - existingModels: ModelStore, - ): ActiveNotificationModel = + /** + * Convert a [ListEntry] into [ActiveNotificationEntryModel]s, and add them to the + * [ActiveNotificationsStore]. Special care is taken to avoid re-allocating models if the result + * would be identical to an existing model (at the expense of additional computations). + */ + fun addListEntry(entry: ListEntry) { + when (entry) { + is GroupEntry -> { + entry.summary?.let { summary -> + val summaryModel = summary.toModel() + val childModels = entry.children.map { it.toModel() } + builder.addNotifGroup( + existingModels.createOrReuse( + key = entry.key, + summary = summaryModel, + children = childModels + ) + ) + } + } + else -> { + entry.representativeEntry?.let { notifEntry -> + builder.addIndividualNotif(notifEntry.toModel()) + } + } + } + } + + private fun NotificationEntry.toModel(): ActiveNotificationModel = existingModels.createOrReuse( key = key, - groupKey = representativeEntry?.sbn?.groupKey, + groupKey = sbn.groupKey, isAmbient = sectionStyleProvider.isMinimized(this), - isRowDismissed = representativeEntry?.isRowDismissed == true, + isRowDismissed = isRowDismissed, isSilent = sectionStyleProvider.isSilent(this), - isLastMessageFromReply = representativeEntry?.isLastMessageFromReply == true, - isSuppressedFromStatusBar = representativeEntry?.shouldSuppressStatusBar() == true, - isPulsing = representativeEntry?.showingPulsing() == true, - aodIcon = representativeEntry?.icons?.aodIcon?.sourceIcon, - shelfIcon = representativeEntry?.icons?.shelfIcon?.sourceIcon, - statusBarIcon = representativeEntry?.icons?.statusBarIcon?.sourceIcon, + isLastMessageFromReply = isLastMessageFromReply, + isSuppressedFromStatusBar = shouldSuppressStatusBar(), + isPulsing = showingPulsing(), + aodIcon = icons.aodIcon?.sourceIcon, + shelfIcon = icons.shelfIcon?.sourceIcon, + statusBarIcon = icons.statusBarIcon?.sourceIcon, ) +} - private fun ModelStore.createOrReuse( - key: String, - groupKey: String?, - isAmbient: Boolean, - isRowDismissed: Boolean, - isSilent: Boolean, - isLastMessageFromReply: Boolean, - isSuppressedFromStatusBar: Boolean, - isPulsing: Boolean, - aodIcon: Icon?, - shelfIcon: Icon?, - statusBarIcon: Icon? - ): ActiveNotificationModel { - return this[key]?.takeIf { - it.isCurrent( - key = key, - groupKey = groupKey, - isAmbient = isAmbient, - isRowDismissed = isRowDismissed, - isSilent = isSilent, - isLastMessageFromReply = isLastMessageFromReply, - isSuppressedFromStatusBar = isSuppressedFromStatusBar, - isPulsing = isPulsing, - aodIcon = aodIcon, - shelfIcon = shelfIcon, - statusBarIcon = statusBarIcon - ) - } - ?: ActiveNotificationModel( - key = key, - groupKey = groupKey, - isAmbient = isAmbient, - isRowDismissed = isRowDismissed, - isSilent = isSilent, - isLastMessageFromReply = isLastMessageFromReply, - isSuppressedFromStatusBar = isSuppressedFromStatusBar, - isPulsing = isPulsing, - aodIcon = aodIcon, - shelfIcon = shelfIcon, - statusBarIcon = statusBarIcon, - ) +private fun ActiveNotificationsStore.createOrReuse( + key: String, + groupKey: String?, + isAmbient: Boolean, + isRowDismissed: Boolean, + isSilent: Boolean, + isLastMessageFromReply: Boolean, + isSuppressedFromStatusBar: Boolean, + isPulsing: Boolean, + aodIcon: Icon?, + shelfIcon: Icon?, + statusBarIcon: Icon? +): ActiveNotificationModel { + return individuals[key]?.takeIf { + it.isCurrent( + key = key, + groupKey = groupKey, + isAmbient = isAmbient, + isRowDismissed = isRowDismissed, + isSilent = isSilent, + isLastMessageFromReply = isLastMessageFromReply, + isSuppressedFromStatusBar = isSuppressedFromStatusBar, + isPulsing = isPulsing, + aodIcon = aodIcon, + shelfIcon = shelfIcon, + statusBarIcon = statusBarIcon + ) + } + ?: ActiveNotificationModel( + key = key, + groupKey = groupKey, + isAmbient = isAmbient, + isRowDismissed = isRowDismissed, + isSilent = isSilent, + isLastMessageFromReply = isLastMessageFromReply, + isSuppressedFromStatusBar = isSuppressedFromStatusBar, + isPulsing = isPulsing, + aodIcon = aodIcon, + shelfIcon = shelfIcon, + statusBarIcon = statusBarIcon, + ) +} + +private fun ActiveNotificationModel.isCurrent( + key: String, + groupKey: String?, + isAmbient: Boolean, + isRowDismissed: Boolean, + isSilent: Boolean, + isLastMessageFromReply: Boolean, + isSuppressedFromStatusBar: Boolean, + isPulsing: Boolean, + aodIcon: Icon?, + shelfIcon: Icon?, + statusBarIcon: Icon? +): Boolean { + return when { + key != this.key -> false + groupKey != this.groupKey -> false + isAmbient != this.isAmbient -> false + isRowDismissed != this.isRowDismissed -> false + isSilent != this.isSilent -> false + isLastMessageFromReply != this.isLastMessageFromReply -> false + isSuppressedFromStatusBar != this.isSuppressedFromStatusBar -> false + isPulsing != this.isPulsing -> false + aodIcon != this.aodIcon -> false + shelfIcon != this.shelfIcon -> false + statusBarIcon != this.statusBarIcon -> false + else -> true } +} - private fun ActiveNotificationModel.isCurrent( - key: String, - groupKey: String?, - isAmbient: Boolean, - isRowDismissed: Boolean, - isSilent: Boolean, - isLastMessageFromReply: Boolean, - isSuppressedFromStatusBar: Boolean, - isPulsing: Boolean, - aodIcon: Icon?, - shelfIcon: Icon?, - statusBarIcon: Icon? - ): Boolean { - return when { - key != this.key -> false - groupKey != this.groupKey -> false - isAmbient != this.isAmbient -> false - isRowDismissed != this.isRowDismissed -> false - isSilent != this.isSilent -> false - isLastMessageFromReply != this.isLastMessageFromReply -> false - isSuppressedFromStatusBar != this.isSuppressedFromStatusBar -> false - isPulsing != this.isPulsing -> false - aodIcon != this.aodIcon -> false - shelfIcon != this.shelfIcon -> false - statusBarIcon != this.statusBarIcon -> false - else -> true - } +private fun ActiveNotificationsStore.createOrReuse( + key: String, + summary: ActiveNotificationModel, + children: List<ActiveNotificationModel>, +): ActiveNotificationGroupModel { + return groups[key]?.takeIf { it.isCurrent(key, summary, children) } + ?: ActiveNotificationGroupModel(key, summary, children) +} + +private fun ActiveNotificationGroupModel.isCurrent( + key: String, + summary: ActiveNotificationModel, + children: List<ActiveNotificationModel>, +): Boolean { + return when { + key != this.key -> false + summary != this.summary -> false + children != this.children -> false + else -> true } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt index 00d873e074b8d2e84ead2952a307cc14cab56b9a..30e2f0e0a57f943b23d298017257507dea534abf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractor.kt @@ -48,7 +48,7 @@ constructor( showPulsing: Boolean = true, ): Flow<Set<ActiveNotificationModel>> { return combine( - activeNotificationsInteractor.notifications, + activeNotificationsInteractor.topLevelRepresentativeNotifications, keyguardViewStateRepository.areNotificationsFullyHidden, ) { notifications, notifsFullyHidden -> notifications diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt index 53631e330d8caf5506be299c60b130248a42ac46..82626acc4b04007825759105af34034ef014be37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt @@ -61,7 +61,7 @@ constructor( darkIconInteractor.tintAreas, darkIconInteractor.tintColor, // Included so that tints are re-applied after entries are changed. - notificationsInteractor.notifications, + notificationsInteractor.topLevelRepresentativeNotifications, ) { areas, tint, _ -> NotificationIconColorLookup { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt index 11c982590477846b15ee69197665c52e33693bb6..538be142b8f2ae7d6f846f0ce930f03a1666201d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.interruption +import android.app.Notification.BubbleMetadata import android.app.Notification.VISIBILITY_PRIVATE import android.app.NotificationManager.IMPORTANCE_DEFAULT import android.app.NotificationManager.IMPORTANCE_HIGH @@ -31,6 +32,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl.MAX_HUN_WHEN_AGE_MS +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE import com.android.systemui.statusbar.policy.BatteryController @@ -180,3 +182,38 @@ class PulseLowImportanceSuppressor() : VisualInterruptionFilter(types = setOf(PULSE), reason = "importance less than DEFAULT") { override fun shouldSuppress(entry: NotificationEntry) = entry.importance < IMPORTANCE_DEFAULT } + +class HunGroupAlertBehaviorSuppressor() : + VisualInterruptionFilter( + types = setOf(PEEK, PULSE), + reason = "suppressive group alert behavior" + ) { + override fun shouldSuppress(entry: NotificationEntry) = + entry.sbn.let { it.isGroup && it.notification.suppressAlertingDueToGrouping() } +} + +class HunJustLaunchedFsiSuppressor() : + VisualInterruptionFilter(types = setOf(PEEK, PULSE), reason = "just launched FSI") { + override fun shouldSuppress(entry: NotificationEntry) = entry.hasJustLaunchedFullScreenIntent() +} + +class BubbleNotAllowedSuppressor() : + VisualInterruptionFilter(types = setOf(BUBBLE), reason = "not allowed") { + override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble() +} + +class BubbleNoMetadataSuppressor() : + VisualInterruptionFilter(types = setOf(BUBBLE), reason = "no bubble metadata") { + + private fun isValidMetadata(metadata: BubbleMetadata?) = + metadata != null && (metadata.intent != null || metadata.shortcutId != null) + + override fun shouldSuppress(entry: NotificationEntry) = !isValidMetadata(entry.bubbleMetadata) +} + +class AlertKeyguardVisibilitySuppressor( + private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider +) : VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "hidden on keyguard") { + override fun shouldSuppress(entry: NotificationEntry) = + keyguardNotificationVisibilityProvider.shouldHideNotification(entry) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt index 7f144bf1461508e81a638937eeca64f93d186cfb..2730683a31c9405e2873efe369c0f293a344085b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt @@ -19,6 +19,7 @@ import android.hardware.display.AmbientDisplayConfiguration import android.os.Handler import android.os.PowerManager import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.settings.UserTracker @@ -41,6 +42,7 @@ constructor( private val batteryController: BatteryController, private val globalSettings: GlobalSettings, private val headsUpManager: HeadsUpManager, + private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider, private val logger: NotificationInterruptLogger, @Main private val mainHandler: Handler, private val powerManager: PowerManager, @@ -65,6 +67,11 @@ constructor( addFilter(PulseEffectSuppressor()) addFilter(PulseLockscreenVisibilityPrivateSuppressor()) addFilter(PulseLowImportanceSuppressor()) + addFilter(BubbleNotAllowedSuppressor()) + addFilter(BubbleNoMetadataSuppressor()) + addFilter(HunGroupAlertBehaviorSuppressor()) + addFilter(HunJustLaunchedFsiSuppressor()) + addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider)) started = true } @@ -100,11 +107,21 @@ constructor( condition.start() } + @VisibleForTesting + fun removeCondition(condition: VisualInterruptionCondition) { + conditions.remove(condition) + } + fun addFilter(filter: VisualInterruptionFilter) { filters.add(filter) filter.start() } + @VisibleForTesting + fun removeFilter(filter: VisualInterruptionFilter) { + filters.remove(filter) + } + override fun makeUnloggedHeadsUpDecision(entry: NotificationEntry): Decision { check(started) return makeHeadsUpDecision(entry) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index 78370baa43116aa991c2def48c61d9981e8ffa15..eb1c1bafae9e59817e2d9fe2762cf1c64762e1fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -17,9 +17,17 @@ package com.android.systemui.statusbar.notification.shared import android.graphics.drawable.Icon -/** Model for entries in the notification stack. */ +/** + * Model for a top-level "entry" in the notification list, either an + * [individual notification][ActiveNotificationModel], or a [group][ActiveNotificationGroupModel]. + */ +sealed class ActiveNotificationEntryModel + +/** + * Model for an individual notification in the notification list. These can appear as either an + * individual top-level notification, or as a child or summary of a [ActiveNotificationGroupModel]. + */ data class ActiveNotificationModel( - /** Notification key associated with this entry. */ val key: String, /** Notification group key associated with this entry. */ val groupKey: String?, @@ -47,4 +55,11 @@ data class ActiveNotificationModel( val shelfIcon: Icon?, /** Icon to display in the status bar. */ val statusBarIcon: Icon?, -) +) : ActiveNotificationEntryModel() + +/** Model for a group of notifications. */ +data class ActiveNotificationGroupModel( + val key: String, + val summary: ActiveNotificationModel, + val children: List<ActiveNotificationModel>, +) : ActiveNotificationEntryModel() diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index fda4133240c21c6032fa5c734c7f501543b1c256..225f12536a33d314b63e2abb5b11ae8ec94ad309 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -810,6 +810,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Bouncer, flowOf(.5f), false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer, null), "reason") @@ -825,7 +826,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Bouncer, SceneKey.Gone, flowOf(.5f), - false + false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") @@ -842,7 +844,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Gone, SceneKey.Bouncer, flowOf(.5f), - false + false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Bouncer, null), "reason") @@ -860,7 +863,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Bouncer, SceneKey.Gone, flowOf(.5f), - false + false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") @@ -876,6 +880,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Lockscreen, flowOf(.5f), false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen, null), "reason") @@ -893,6 +898,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { SceneKey.Gone, flowOf(.5f), false, + isUserInputOngoing = flowOf(false), ) runCurrent() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone, null), "reason") diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 728102d806fa399d58931d099644ee1fed776585..0b38c4a1b12626c9b7d7e71e73013042f01b2a16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -43,6 +43,7 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.animation.AccelerateInterpolator; import androidx.test.InstrumentationRegistry; +import androidx.test.filters.FlakyTest; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; @@ -67,6 +68,7 @@ import java.util.concurrent.atomic.AtomicReference; @LargeTest @RunWith(AndroidTestingRunner.class) +@FlakyTest(bugId = 308501761) public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 5e7b85779599239038cc7d8125add786b9c28540..2d95b09cbf0e652158e7c00579a053d8346d75dd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -54,6 +54,7 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.statusbar.events.ANIMATING_OUT import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -210,6 +211,16 @@ open class AuthContainerViewTest : SysuiTestCase() { ) } + @Test + fun testIgnoresAnimatedInWhenDialogAnimatingOut() { + val container = initializeFingerprintContainer(addToView = false) + container.mContainerState = ANIMATING_OUT + container.addToView() + waitForIdleSync() + + verify(callback, never()).onDialogAnimatedIn(anyLong(), anyBoolean()) + } + @Test fun testDismissBeforeIntroEnd() { val container = initializeFingerprintContainer() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt index 1e7a3d3ef6ffb32449d7d3bdb07fd60e459b5c26..67d3a20bd5c566dac9c59470d1282457e4506e0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt @@ -37,8 +37,6 @@ import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue import com.android.systemui.dump.logcatLogBuffer -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.flags.Flags.REST_TO_UNLOCK import com.android.systemui.log.SideFpsLogger import com.android.systemui.res.R import com.android.systemui.util.mockito.whenever @@ -94,7 +92,6 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { whenever(displayStateInteractor.currentRotation).thenReturn(currentRotation) contextDisplayInfo.uniqueId = "current-display" - val featureFlags = FakeFeatureFlagsClassic().apply { set(REST_TO_UNLOCK, true) } whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser) .thenReturn(isRestToUnlockEnabled) underTest = @@ -103,7 +100,6 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { fingerprintRepository, windowManager, displayStateInteractor, - featureFlags, Optional.of(fingerprintInteractiveToAuthProvider), SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) @@ -136,7 +132,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { @Test fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() = testScope.runTest { - assertThat(collectLastValue(underTest.authenticationDuration)()) + assertThat(underTest.authenticationDuration) .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration)) } @@ -165,7 +161,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(1000) assertThat(sensorLocation!!.top).isEqualTo(200) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -193,7 +189,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(500) assertThat(sensorLocation!!.top).isEqualTo(1000) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -221,7 +217,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(200) assertThat(sensorLocation!!.top).isEqualTo(0) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -274,7 +270,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(500) assertThat(sensorLocation!!.top).isEqualTo(0) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -301,7 +297,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(0) assertThat(sensorLocation!!.top).isEqualTo(400) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -328,7 +324,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(400) assertThat(sensorLocation!!.top).isEqualTo(800) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -355,7 +351,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { assertThat(sensorLocation!!.left).isEqualTo(800) assertThat(sensorLocation!!.top).isEqualTo(500) assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) - assertThat(sensorLocation!!.width).isEqualTo(100) + assertThat(sensorLocation!!.length).isEqualTo(100) } @Test @@ -381,10 +377,14 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { rotation: DisplayRotation, sensorWidth: Int ) { - overrideResource(R.integer.config_sfpsSensorWidth, sensorWidth) setupDisplayDimensions(width, height) currentRotation.value = rotation - setupFingerprint(x = sensorLocationX, y = sensorLocationY, displayId = "expanded_display") + setupFingerprint( + x = sensorLocationX, + y = sensorLocationY, + displayId = "expanded_display", + sensorRadius = sensorWidth / 2 + ) } private fun setupDisplayDimensions(displayWidth: Int, displayHeight: Int) { @@ -392,7 +392,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { .thenReturn( WindowMetrics( Rect(0, 0, displayWidth, displayHeight), - mock(WindowInsets::class.java) + mock(WindowInsets::class.java), ) ) } @@ -401,7 +401,8 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { fingerprintSensorType: FingerprintSensorType = FingerprintSensorType.POWER_BUTTON, x: Int = 0, y: Int = 0, - displayId: String = "display_id_1" + displayId: String = "display_id_1", + sensorRadius: Int = 150 ) { contextDisplayInfo.uniqueId = displayId fingerprintRepository.setProperties( @@ -415,14 +416,14 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { "someOtherDisplayId", x + 100, y + 100, - 0, + sensorRadius, ), displayId to SensorLocationInternal( displayId, x, y, - 0, + sensorRadius, ) ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 8e21f294a3615a74ab545bd8affbec2e07650cc3..2f17b6fa35edd2330366538e85554668ecc78a5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -86,4 +87,48 @@ class CommunalInteractorTest : SysuiTestCase() { val interactor = CommunalInteractor(communalRepository, widgetRepository) assertThat(interactor.isCommunalEnabled).isFalse() } + + @Test + fun listensToSceneChange() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + var desiredScene = collectLastValue(interactor.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(CommunalSceneKey.Blank) + + val targetScene = CommunalSceneKey.Communal + communalRepository.setDesiredScene(targetScene) + desiredScene = collectLastValue(interactor.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(targetScene) + } + + @Test + fun updatesScene() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + val targetScene = CommunalSceneKey.Communal + + interactor.onSceneChanged(targetScene) + + val desiredScene = collectLastValue(communalRepository.desiredScene) + runCurrent() + assertThat(desiredScene()).isEqualTo(targetScene) + } + + @Test + fun isCommunalShowing() = + testScope.runTest { + val interactor = CommunalInteractor(communalRepository, widgetRepository) + + var isCommunalShowing = collectLastValue(interactor.isCommunalShowing) + runCurrent() + assertThat(isCommunalShowing()).isEqualTo(false) + + interactor.onSceneChanged(CommunalSceneKey.Communal) + + isCommunalShowing = collectLastValue(interactor.isCommunalShowing) + runCurrent() + assertThat(isCommunalShowing()).isEqualTo(true) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 0a9a15e06b1bea53c100f133fc8ec03ce695a249..61d1502f307e0403a8d5e9fcae757c1e29a99837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -21,16 +21,23 @@ import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.settings.UserTracker import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before @@ -46,18 +53,28 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - + private lateinit var testScope: TestScope private lateinit var underTest: CommunalTutorialInteractor private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var keyguardInteractor: KeyguardInteractor private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository + private lateinit var sceneContainerFlags: FakeSceneContainerFlags + private lateinit var communalInteractor: CommunalInteractor + private lateinit var communalRepository: FakeCommunalRepository + + private val utils = SceneTestUtils(this) + private lateinit var sceneInteractor: SceneInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) + sceneInteractor = utils.sceneInteractor() + testScope = utils.testScope + sceneContainerFlags = utils.sceneContainerFlags.apply { enabled = false } + communalRepository = FakeCommunalRepository(isCommunalEnabled = true) + communalInteractor = CommunalInteractor(communalRepository, FakeCommunalWidgetRepository()) + val withDeps = KeyguardInteractorFactory.create() keyguardInteractor = withDeps.keyguardInteractor keyguardRepository = withDeps.repository @@ -65,8 +82,12 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { underTest = CommunalTutorialInteractor( - keyguardInteractor = keyguardInteractor, + scope = testScope.backgroundScope, communalTutorialRepository = communalTutorialRepository, + keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, + sceneContainerFlags = sceneContainerFlags, + sceneInteractor = sceneInteractor, ) whenever(userTracker.userHandle).thenReturn(mock()) @@ -87,6 +108,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) assertThat(isTutorialAvailable).isFalse() } @@ -97,6 +119,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) assertThat(isTutorialAvailable).isTrue() } @@ -107,7 +130,188 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) assertThat(isTutorialAvailable).isTrue() } + + /* Testing tutorial states with transitions when flexiglass off */ + @Test + fun tutorialState_notStartedAndCommunalSceneShowing_tutorialStarted() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Communal) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test + fun tutorialState_startedAndCommunalSceneShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Communal) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test + fun tutorialState_completedAndCommunalSceneShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Communal) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialState_notStartedAndCommunalSceneNotShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Blank) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_NOT_STARTED) + } + + @Test + fun tutorialState_startedAndCommunalSceneNotShowing_tutorialCompleted() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Blank) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialState_completedAndCommunalSceneNotShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(communalInteractor.desiredScene) + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + + assertThat(currentScene).isEqualTo(CommunalSceneKey.Blank) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } + + /* Testing tutorial states with transitions when flexiglass on */ + @Test + fun tutorialState_notStartedCommunalSceneShowingAndFlexiglassOn_tutorialStarted() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Communal), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Communal)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test + fun tutorialState_startedCommunalSceneShowingAndFlexiglassOn_stateWillNotUpdate() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Communal), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Communal)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test + fun tutorialState_completedCommunalSceneShowingAndFlexiglassOn_stateWillNotUpdate() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Communal), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Communal)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialState_notStartedCommunalSceneNotShowingAndFlexiglassOn_stateWillNotUpdate() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_NOT_STARTED) + } + + @Test + fun tutorialState_startedCommunalSceneNotShowingAndFlexiglassOn_tutorialCompleted() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Communal), "reason") + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } + + @Test + fun tutorialState_completedCommunalSceneNotShowingAndFlexiglassOn_stateWillNotUpdate() = + testScope.runTest { + sceneContainerFlags.enabled = true + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + val currentScene by collectLastValue(sceneInteractor.desiredScene) + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Communal), "reason") + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") + + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt index d80dd76b23e576255ec5c8debf2ccc4da09c337a..806930d091b17a8afce6a217efe2ce41a4dbab76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt @@ -379,6 +379,32 @@ class DisplayRepositoryTest : SysuiTestCase() { assertThat(pendingDisplay).isNull() } + @Test + fun pendingDisplay_afterConfigChanged_doesNotChange() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1, TYPE_EXTERNAL) + val initialPendingDisplay: DisplayRepository.PendingDisplay? = pendingDisplay + assertThat(pendingDisplay).isNotNull() + sendOnDisplayChanged(1) + + assertThat(initialPendingDisplay).isEqualTo(pendingDisplay) + } + + @Test + fun pendingDisplay_afterNewHigherDisplayConnected_changes() = + testScope.runTest { + val pendingDisplay by lastPendingDisplay() + + sendOnDisplayConnected(1, TYPE_EXTERNAL) + val initialPendingDisplay: DisplayRepository.PendingDisplay? = pendingDisplay + assertThat(pendingDisplay).isNotNull() + sendOnDisplayConnected(2, TYPE_EXTERNAL) + + assertThat(initialPendingDisplay).isNotEqualTo(pendingDisplay) + } + @Test fun onPendingDisplay_OneInternalAndOneExternalDisplay_internalIgnored() = testScope.runTest { @@ -466,6 +492,10 @@ class DisplayRepositoryTest : SysuiTestCase() { connectedDisplayListener.value.onDisplayConnected(id) } + private fun sendOnDisplayChanged(id: Int) { + connectedDisplayListener.value.onDisplayChanged(id) + } + private fun setDisplays(displays: List<Display>) { whenever(displayManager.displays).thenReturn(displays.toTypedArray()) displays.forEach { display -> diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt index f51745b9f17c756f679d7065a06ea2553ddba179..b589a2ac8b136e70250e10145da34ca4bf895409 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt @@ -22,7 +22,6 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Resources import android.content.res.Resources.NotFoundException import android.test.suitebuilder.annotation.SmallTest -import com.android.systemui.FakeFeatureFlagsImpl import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -66,7 +65,6 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { private lateinit var broadcastReceiver: BroadcastReceiver private lateinit var clearCacheAction: Consumer<String> private val serverFlagReader = ServerFlagReaderFake() - private val fakeGantryFlags = FakeFeatureFlagsImpl() private val teamfoodableFlagA = UnreleasedFlag(name = "a", namespace = "test", teamfood = true) private val teamfoodableFlagB = ReleasedFlag(name = "b", namespace = "test", teamfood = true) @@ -74,7 +72,6 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - fakeGantryFlags.setFlag("com.android.systemui.sysui_teamfood", false) flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA) flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB) mFeatureFlagsClassicDebug = @@ -86,7 +83,6 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { resources, serverFlagReader, flagMap, - fakeGantryFlags, restarter ) mFeatureFlagsClassicDebug.init() @@ -134,7 +130,7 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { @Test fun teamFoodFlag_True() { - fakeGantryFlags.setFlag("com.android.systemui.sysui_teamfood", true) + mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD) assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue() assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isTrue() @@ -149,7 +145,7 @@ class FeatureFlagsClassicDebugTest : SysuiTestCase() { .thenReturn(true) whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any())) .thenReturn(false) - fakeGantryFlags.setFlag("com.android.systemui.sysui_teamfood", true) + mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD) assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue() assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 27325d3f4ecffd730f2f2225b9bc6bb85990ee89..ad2ec72468adc3d75c842740e96b6d42d036d130 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -203,7 +203,8 @@ class KeyguardInteractorTest : SysuiTestCase() { fromScene = SceneKey.Gone, toScene = SceneKey.Lockscreen, progress = flowOf(0f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) runCurrent() assertThat(isAnimate).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 5296f1abd756ec57b5f835af016feddc659671b6..5bfe56931bb42706e209db93296d44409e216eaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -25,6 +25,10 @@ import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle @@ -46,6 +50,11 @@ import com.android.systemui.util.mockito.nullable import com.android.systemui.util.settings.FakeSettings import com.android.systemui.utils.os.FakeHandler 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.Assert.assertNotNull import org.junit.Before import org.junit.Rule @@ -63,6 +72,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @@ -71,6 +81,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var lockHost: MediaHost @Mock private lateinit var qsHost: MediaHost @Mock private lateinit var qqsHost: MediaHost + @Mock private lateinit var hubModeHost: MediaHost @Mock private lateinit var bypassController: KeyguardBypassController @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController @@ -93,10 +104,15 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var mediaHierarchyManager: MediaHierarchyManager private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() + private val communalRepository = FakeCommunalRepository(isCommunalEnabled = true) + private val communalInteractor = + CommunalInteractor(communalRepository, FakeCommunalWidgetRepository()) private val notifPanelEvents = ShadeExpansionStateManager() private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setup() { @@ -117,11 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() { mediaDataManager, keyguardViewController, dreamOverlayStateController, + communalInteractor, configurationController, wakefulnessLifecycle, notifPanelEvents, settings, fakeHandler, + testScope.backgroundScope, ResourcesSplitShadeStateController(), logger, ) @@ -131,6 +149,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP) setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) + setupHost(hubModeHost, MediaHierarchyManager.LOCATION_COMMUNAL_HUB, COMMUNAL_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) @@ -474,6 +493,33 @@ class MediaHierarchyManagerTest : SysuiTestCase() { ) } + @Test + fun testCommunalLocation() = + testScope.runTest { + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + runCurrent() + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB), + nullable(), + eq(false), + anyLong(), + anyLong() + ) + clearInvocations(mediaCarouselController) + + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + runCurrent() + verify(mediaCarouselController) + .onDesiredLocationChanged( + eq(MediaHierarchyManager.LOCATION_QQS), + any(MediaHostState::class.java), + eq(false), + anyLong(), + anyLong() + ) + } + @Test fun testQsExpandedChanged_noQqsMedia() { // When we are looking at QQS with active media @@ -538,5 +584,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private const val QQS_TOP = 123 private const val QS_TOP = 456 private const val LOCKSCREEN_TOP = 789 + private const val COMMUNAL_TOP = 111 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/CustomTilePackageUpdatesRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/CustomTilePackageUpdatesRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4a221134ce6778a1adcc4ce7d2932d16870c7959 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/custom/CustomTilePackageUpdatesRepositoryTest.kt @@ -0,0 +1,119 @@ +/* + * 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.qs.tiles.impl.custom + +import android.content.ComponentName +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.qs.external.TileLifecycleManager +import com.android.systemui.qs.external.TileServiceManager +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository +import com.android.systemui.qs.tiles.impl.custom.data.repository.FakeCustomTileDefaultsRepository.DefaultsRequest +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +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.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CustomTilePackageUpdatesRepositoryTest : SysuiTestCase() { + + @Mock private lateinit var tileServiceManager: TileServiceManager + + @Captor + private lateinit var listenerCaptor: ArgumentCaptor<TileLifecycleManager.TileChangeListener> + + private val defaultsRepository = FakeCustomTileDefaultsRepository() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: CustomTilePackageUpdatesRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + CustomTilePackageUpdatesRepositoryImpl( + TileSpec.create(COMPONENT_1), + USER, + tileServiceManager, + defaultsRepository, + testScope.backgroundScope, + ) + } + + @Test + fun packageChangesUpdatesDefaults() = + testScope.runTest { + val events = mutableListOf<Unit>() + underTest.packageChanges.onEach { events.add(it) }.launchIn(backgroundScope) + runCurrent() + verify(tileServiceManager).setTileChangeListener(capture(listenerCaptor)) + + emitPackageChange() + runCurrent() + + assertThat(events).hasSize(1) + assertThat(defaultsRepository.defaultsRequests).isNotEmpty() + assertThat(defaultsRepository.defaultsRequests.last()) + .isEqualTo(DefaultsRequest(USER, COMPONENT_1, true)) + } + + @Test + fun packageChangesEmittedOnlyForTheTile() = + testScope.runTest { + val events = mutableListOf<Unit>() + underTest.packageChanges.onEach { events.add(it) }.launchIn(backgroundScope) + runCurrent() + verify(tileServiceManager).setTileChangeListener(capture(listenerCaptor)) + + emitPackageChange(COMPONENT_2) + runCurrent() + + assertThat(events).isEmpty() + } + + private fun emitPackageChange(componentName: ComponentName = COMPONENT_1) { + listenerCaptor.value.onTileChanged(componentName) + } + + private companion object { + val USER = UserHandle(0) + val COMPONENT_1 = ComponentName("pkg.test.1", "cls.test") + val COMPONENT_2 = ComponentName("pkg.test.2", "cls.test") + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 88a5c17f4994bdd26ad0a3d0e9fba117d0f6f8c5..9c0456cfdb4cf58916eb0f5f995e8b206ef6cbd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -54,6 +54,7 @@ import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -466,7 +467,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { fromScene = getCurrentSceneInUi(), toScene = to.key, progress = progressFlow, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index 432bd0f2a0507b577b94e8de24f6ab9e497fa452..d669006c68d33e1b34aca2e70e5200a1b5ad79b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -51,6 +52,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { SceneKey.Lockscreen, SceneKey.Bouncer, SceneKey.Gone, + SceneKey.Communal, ) ) } @@ -119,7 +121,8 @@ class SceneContainerRepositoryTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = SceneKey.Shade, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(reflectedTransitionState).isEqualTo(transitionState.value) diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 8b23d183f1a6e5f3675a5561e7e181a342b787c7..3f032a45df415a1e82f394c426d2b08185ddcce4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -83,7 +83,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = SceneKey.Shade, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(reflectedTransitionState).isEqualTo(transitionState.value) @@ -121,7 +122,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = underTest.desiredScene.value.key, toScene = SceneKey.Shade, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(transitionTo).isEqualTo(SceneKey.Shade) @@ -158,7 +160,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = SceneKey.Gone, toScene = SceneKey.Lockscreen, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) val transitioning by @@ -177,7 +180,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = SceneKey.Shade, toScene = SceneKey.QuickSettings, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) underTest.setTransitionState(transitionState) @@ -194,7 +198,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = SceneKey.Shade, toScene = SceneKey.Lockscreen, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) val transitioning by @@ -222,7 +227,8 @@ class SceneInteractorTest : SysuiTestCase() { fromScene = SceneKey.Shade, toScene = SceneKey.Lockscreen, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(transitioning).isTrue() @@ -230,6 +236,95 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(transitioning).isFalse() } + @Test + fun isTransitionUserInputOngoing_idle_false() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(SceneKey.Shade) + ) + val isTransitionUserInputOngoing by + collectLastValue(underTest.isTransitionUserInputOngoing) + underTest.setTransitionState(transitionState) + + assertThat(isTransitionUserInputOngoing).isFalse() + } + + @Test + fun isTransitionUserInputOngoing_transition_true() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.Lockscreen, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + ) + val isTransitionUserInputOngoing by + collectLastValue(underTest.isTransitionUserInputOngoing) + underTest.setTransitionState(transitionState) + + assertThat(isTransitionUserInputOngoing).isTrue() + } + + @Test + fun isTransitionUserInputOngoing_updateMidTransition_false() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.Lockscreen, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + ) + val isTransitionUserInputOngoing by + collectLastValue(underTest.isTransitionUserInputOngoing) + underTest.setTransitionState(transitionState) + + assertThat(isTransitionUserInputOngoing).isTrue() + + transitionState.value = + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.Lockscreen, + progress = flowOf(0.6f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + ) + + assertThat(isTransitionUserInputOngoing).isFalse() + } + + @Test + fun isTransitionUserInputOngoing_updateOnIdle_false() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.Lockscreen, + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + ) + val isTransitionUserInputOngoing by + collectLastValue(underTest.isTransitionUserInputOngoing) + underTest.setTransitionState(transitionState) + + assertThat(isTransitionUserInputOngoing).isTrue() + + transitionState.value = ObservableTransitionState.Idle(scene = SceneKey.Lockscreen) + + assertThat(isTransitionUserInputOngoing).isFalse() + } + @Test fun isVisible() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index c1f2d0cc518f5c57c6bd88983cdcf402b64dd5d2..c0b586195eca9d2e5fa69f1d27571ff7dce63779 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -115,7 +115,8 @@ class SceneContainerStartableTest : SysuiTestCase() { fromScene = SceneKey.Gone, toScene = SceneKey.Shade, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(isVisible).isTrue() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") @@ -128,7 +129,8 @@ class SceneContainerStartableTest : SysuiTestCase() { fromScene = SceneKey.Shade, toScene = SceneKey.Gone, progress = flowOf(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) assertThat(isVisible).isTrue() sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt index 0bed4d0d376a353026c940af71624cf83b5a1e57..32a38bd1faa1a44cae2e18d777e8a664d1eafeac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt @@ -76,7 +76,6 @@ internal class SceneContainerFlagsTest( underTest = SceneContainerFlagsImpl( featureFlagsClassic = featureFlags, - featureFlags = aconfigFlags, isComposeAvailable = testCase.isComposeAvailable, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 4e3e165c83bb6e392b6769edaddaad294d202038..5459779121c98e72522d58a832b9539cd158cfc0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -97,6 +97,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope @@ -111,9 +112,8 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import java.util.Optional import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -140,6 +140,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @Mock private lateinit var statusBarWindowStateController: StatusBarWindowStateController + @Mock private lateinit var quickSettingsController: QuickSettingsController @Mock private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController @Mock private lateinit var lockIconViewController: LockIconViewController @@ -166,7 +167,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor private val notificationLaunchAnimationRepository = NotificationLaunchAnimationRepository() private val notificationLaunchAnimationInteractor = - NotificationLaunchAnimationInteractor(notificationLaunchAnimationRepository) + NotificationLaunchAnimationInteractor(notificationLaunchAnimationRepository) private lateinit var fakeClock: FakeSystemClock private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler> @@ -274,6 +275,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { ), BouncerLogger(logcatLogBuffer("BouncerLog")), sysUIKeyEventHandler, + quickSettingsController, primaryBouncerInteractor, alternateBouncerInteractor, mSelectedUserInteractor, @@ -460,9 +462,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { // AND alternate bouncer doesn't want the touch whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) .thenReturn(false) + // AND quick settings controller doesn't want it + whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any())) + .thenReturn(false) // AND the lock icon wants the touch - whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)) - .thenReturn(true) + whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(true) featureFlagsClassic.set(MIGRATE_NSSL, true) @@ -476,10 +480,31 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) // AND alternate bouncer doesn't want the touch whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) - .thenReturn(false) + .thenReturn(false) // AND the lock icon does NOT want the touch - whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)) - .thenReturn(false) + whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(false) + // AND quick settings controller doesn't want it + whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any())) + .thenReturn(false) + + featureFlagsClassic.set(MIGRATE_NSSL, true) + + // THEN touch should NOT be intercepted by NotificationShade + assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue() + } + + @Test + fun shouldInterceptTouchEvent_dozing_touchInStatusBar_touchIntercepted() { + // GIVEN dozing + whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) + // AND alternate bouncer doesn't want the touch + whenever(statusBarKeyguardViewManager.shouldInterceptTouchEvent(DOWN_EVENT)) + .thenReturn(false) + // AND the lock icon does NOT want the touch + whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)).thenReturn(false) + // AND quick settings controller DOES want it + whenever(quickSettingsController.shouldQuickSettingsIntercept(any(), any(), any())) + .thenReturn(true) featureFlagsClassic.set(MIGRATE_NSSL, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 3d5d26ab194e8023a88dab039eeff114c915b2de..a6ab6a51bfa708a8660740b64c444f411ffa0583 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -121,6 +121,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { @Mock private lateinit var notificationStackScrollLayout: NotificationStackScrollLayout @Mock private lateinit var notificationShadeDepthController: NotificationShadeDepthController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController + @Mock private lateinit var quickSettingsController: QuickSettingsController @Mock private lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController @@ -264,6 +265,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { ), BouncerLogger(logcatLogBuffer("BouncerLog")), Mockito.mock(SysUIKeyEventHandler::class.java), + quickSettingsController, primaryBouncerInteractor, alternateBouncerInteractor, mSelectedUserInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt index 3a260ae374c63d5f53bdfde64e333140113dd422..8b8a6258589885fe7f3c903a7d0d5eb58d6232c1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt @@ -60,6 +60,7 @@ import dagger.BindsInstance import dagger.Component import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -495,7 +496,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = key, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -532,7 +534,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = key, toScene = SceneKey.Lockscreen, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -568,7 +571,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = SceneKey.Shade, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -844,7 +848,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = key, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -881,7 +886,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = SceneKey.Lockscreen, toScene = key, progress = progress, - isUserInputDriven = true, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -918,7 +924,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = key, toScene = SceneKey.Lockscreen, progress = progress, - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -955,7 +962,8 @@ class ShadeInteractorTest : SysuiTestCase() { fromScene = key, toScene = SceneKey.Lockscreen, progress = progress, - isUserInputDriven = true, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) @@ -990,8 +998,9 @@ class ShadeInteractorTest : SysuiTestCase() { ObservableTransitionState.Transition( fromScene = SceneKey.Lockscreen, toScene = SceneKey.QuickSettings, - progress = progress, - isUserInputDriven = true, + progress = MutableStateFlow(0f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), ) ) sceneInteractor.setTransitionState(transitionState) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index f98267b639868f9ce841487ef7c09f1240a9ef07..02d15de4d95396e3e02ee7df22e9afbe47beb2ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -20,6 +20,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -89,7 +90,8 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { fromScene = SceneKey.Shade, toScene = SceneKey.QuickSettings, progress = MutableStateFlow(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) ) @@ -107,7 +109,8 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { fromScene = SceneKey.QuickSettings, toScene = SceneKey.Shade, progress = MutableStateFlow(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) ) @@ -125,7 +128,8 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { fromScene = SceneKey.Gone, toScene = SceneKey.Shade, progress = MutableStateFlow(0.5f), - isUserInputDriven = false, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), ) ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt index ca8ea4eb9d1a5d89d4944f3951a6dad9594cafd8..b86f8410fb7f686d7caac1e198afa7aa89c624b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt @@ -19,6 +19,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.byKey import com.android.systemui.util.mockito.mock @@ -40,9 +41,19 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { @Test fun setRenderedList_preservesOrdering() = runTest { - val notifs by collectLastValue(notifsInteractor.notifications) + val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications) val keys = (1..50).shuffled().map { "$it" } - val entries = keys.map { mock<ListEntry> { whenever(key).thenReturn(it) } } + val entries = + keys.map { + mock<ListEntry> { + val mockRep = mock<NotificationEntry> { + whenever(key).thenReturn(it) + whenever(sbn).thenReturn(mock()) + whenever(icons).thenReturn(mock()) + } + whenever(representativeEntry).thenReturn(mockRep) + } + } underTest.setRenderedList(entries) assertThat(notifs).comparingElementsUsing(byKey).containsExactlyElementsIn(keys).inOrder() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt index ec80e5f8821c26cd9072155412ebb3125e6b2e34..f8252a721b32fe463e23a856800afc3726f3b13c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository import com.android.systemui.statusbar.notification.shared.activeNotificationModel import com.android.systemui.statusbar.notification.shared.byIsAmbient @@ -77,7 +78,9 @@ class NotificationIconsInteractorTest : SysuiTestCase() { fun setup() = with(testComponent) { activeNotificationListRepository.activeNotifications.value = - testIcons.associateBy { it.key } + ActiveNotificationsStore.Builder() + .apply { testIcons.forEach(::addIndividualNotif) } + .build() } @Test @@ -196,7 +199,9 @@ class AlwaysOnDisplayNotificationIconsInteractorTest : SysuiTestCase() { fun setup() = with(testComponent) { activeNotificationListRepository.activeNotifications.value = - testIcons.associateBy { it.key } + ActiveNotificationsStore.Builder() + .apply { testIcons.forEach(::addIndividualNotif) } + .build() } @Test @@ -318,7 +323,9 @@ class StatusBarNotificationIconsInteractorTest : SysuiTestCase() { fun setup() = with(testComponent) { activeNotificationListRepository.activeNotifications.value = - testIcons.associateBy { it.key } + ActiveNotificationsStore.Builder() + .apply { testIcons.forEach(::addIndividualNotif) } + .build() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt index ba68fbb981e6d0c73d2b73e99fa7fb4d8432e9f3..44acac8620ee69298bc4044b8da83a3733466a82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt @@ -42,6 +42,7 @@ import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationIconViewStateRepository import com.android.systemui.statusbar.notification.shared.activeNotificationModel import com.android.systemui.statusbar.phone.DozeParameters @@ -342,14 +343,17 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() { val icon: Icon = mock() shadeRepository.setLegacyShadeExpansion(0f) activeNotificationsRepository.activeNotifications.value = - listOf( - activeNotificationModel( - key = "notif1", - groupKey = "group", - statusBarIcon = icon + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif( + activeNotificationModel( + key = "notif1", + groupKey = "group", + statusBarIcon = icon + ) ) - ) - .associateBy { it.key } + } + .build() val isolatedIcon by collectLastValue(underTest.isolatedIcon) runCurrent() @@ -368,14 +372,17 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() { val icon: Icon = mock() shadeRepository.setLegacyShadeExpansion(.5f) activeNotificationsRepository.activeNotifications.value = - listOf( - activeNotificationModel( - key = "notif1", - groupKey = "group", - statusBarIcon = icon + ActiveNotificationsStore.Builder() + .apply { + addIndividualNotif( + activeNotificationModel( + key = "notif1", + groupKey = "group", + statusBarIcon = icon + ) ) - ) - .associateBy { it.key } + } + .build() val isolatedIcon by collectLastValue(underTest.isolatedIcon) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt index ff89bdb6dbdea5d001b83aa89535062b1d35eb3e..80d941a40cb54445063cb8a353d6aca9cdaebf0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt @@ -18,6 +18,11 @@ package com.android.systemui.statusbar.notification.interruption import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE +import org.junit.Test import org.junit.runner.RunWith @SmallTest @@ -29,6 +34,7 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro batteryController, globalSettings, headsUpManager, + keyguardNotificationVisibilityProvider, logger, mainHandler, powerManager, @@ -37,4 +43,179 @@ class VisualInterruptionDecisionProviderImplTest : VisualInterruptionDecisionPro userTracker, ) } + + @Test + fun testNothingCondition_suppressesNothing() { + withCondition(TestCondition(types = emptySet()) { true }) { + assertPeekNotSuppressed() + assertPulseNotSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testNothingFilter_suppressesNothing() { + withFilter(TestFilter(types = emptySet()) { true }) { + assertPeekNotSuppressed() + assertPulseNotSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testPeekCondition_suppressesOnlyPeek() { + withCondition(TestCondition(types = setOf(PEEK)) { true }) { + assertPeekSuppressed() + assertPulseNotSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testPeekFilter_suppressesOnlyPeek() { + withFilter(TestFilter(types = setOf(PEEK)) { true }) { + assertPeekSuppressed() + assertPulseNotSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testPulseCondition_suppressesOnlyPulse() { + withCondition(TestCondition(types = setOf(PULSE)) { true }) { + assertPeekNotSuppressed() + assertPulseSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testPulseFilter_suppressesOnlyPulse() { + withFilter(TestFilter(types = setOf(PULSE)) { true }) { + assertPeekNotSuppressed() + assertPulseSuppressed() + assertBubbleNotSuppressed() + } + } + + @Test + fun testBubbleCondition_suppressesOnlyBubble() { + withCondition(TestCondition(types = setOf(BUBBLE)) { true }) { + assertPeekNotSuppressed() + assertPulseNotSuppressed() + assertBubbleSuppressed() + } + } + + @Test + fun testBubbleFilter_suppressesOnlyBubble() { + withFilter(TestFilter(types = setOf(BUBBLE)) { true }) { + assertPeekNotSuppressed() + assertPulseNotSuppressed() + assertBubbleSuppressed() + } + } + + @Test + fun testCondition_differentState() { + ensurePeekState() + val entry = buildPeekEntry() + + var stateShouldSuppress = false + withCondition(TestCondition(types = setOf(PEEK)) { stateShouldSuppress }) { + assertShouldHeadsUp(entry) + + stateShouldSuppress = true + assertShouldNotHeadsUp(entry) + + stateShouldSuppress = false + assertShouldHeadsUp(entry) + } + } + + @Test + fun testFilter_differentState() { + ensurePeekState() + val entry = buildPeekEntry() + + var stateShouldSuppress = false + withFilter(TestFilter(types = setOf(PEEK)) { stateShouldSuppress }) { + assertShouldHeadsUp(entry) + + stateShouldSuppress = true + assertShouldNotHeadsUp(entry) + + stateShouldSuppress = false + assertShouldHeadsUp(entry) + } + } + + @Test + fun testFilter_differentNotif() { + ensurePeekState() + + val suppressedEntry = buildPeekEntry() + val unsuppressedEntry = buildPeekEntry() + + withFilter(TestFilter(types = setOf(PEEK)) { it == suppressedEntry }) { + assertShouldNotHeadsUp(suppressedEntry) + assertShouldHeadsUp(unsuppressedEntry) + } + } + + private fun assertPeekSuppressed() { + ensurePeekState() + assertShouldNotHeadsUp(buildPeekEntry()) + } + + private fun assertPeekNotSuppressed() { + ensurePeekState() + assertShouldHeadsUp(buildPeekEntry()) + } + + private fun assertPulseSuppressed() { + ensurePulseState() + assertShouldNotHeadsUp(buildPulseEntry()) + } + + private fun assertPulseNotSuppressed() { + ensurePulseState() + assertShouldHeadsUp(buildPulseEntry()) + } + + private fun assertBubbleSuppressed() { + ensureBubbleState() + assertShouldNotBubble(buildBubbleEntry()) + } + + private fun assertBubbleNotSuppressed() { + ensureBubbleState() + assertShouldBubble(buildBubbleEntry()) + } + + private fun withCondition(condition: VisualInterruptionCondition, block: () -> Unit) { + provider.addCondition(condition) + block() + provider.removeCondition(condition) + } + + private fun withFilter(filter: VisualInterruptionFilter, block: () -> Unit) { + provider.addFilter(filter) + block() + provider.removeFilter(filter) + } + + private class TestCondition( + types: Set<VisualInterruptionType>, + val onShouldSuppress: () -> Boolean + ) : VisualInterruptionCondition(types = types, reason = "") { + override fun shouldSuppress(): Boolean = onShouldSuppress() + } + + private class TestFilter( + types: Set<VisualInterruptionType>, + val onShouldSuppress: (NotificationEntry) -> Boolean = { true } + ) : VisualInterruptionFilter(types = types, reason = "") { + override fun shouldSuppress(entry: NotificationEntry) = onShouldSuppress(entry) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt index df12289657496c5b630e10ef1b2d27d0f1b38ab9..7f12b22f2b4ec3199bc92c1a2de5d5d2e99acf91 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt @@ -20,6 +20,9 @@ import android.app.ActivityManager import android.app.Notification import android.app.Notification.BubbleMetadata import android.app.Notification.FLAG_BUBBLE +import android.app.Notification.GROUP_ALERT_ALL +import android.app.Notification.GROUP_ALERT_CHILDREN +import android.app.Notification.GROUP_ALERT_SUMMARY import android.app.Notification.VISIBILITY_PRIVATE import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_DEFAULT @@ -305,10 +308,132 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { assertShouldNotHeadsUp(buildPulseEntry { importance = IMPORTANCE_LOW }) } + private fun withPeekAndPulseEntry( + extendEntry: EntryBuilder.() -> Unit, + block: (NotificationEntry) -> Unit + ) { + ensurePeekState() + block(buildPeekEntry(extendEntry)) + + ensurePulseState() + block(buildPulseEntry(extendEntry)) + } + + @Test + fun testShouldHeadsUp_groupedSummaryNotif_groupAlertAll() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = true + groupAlertBehavior = GROUP_ALERT_ALL + }) { + assertShouldHeadsUp(it) + } + } + + @Test + fun testShouldHeadsUp_groupedSummaryNotif_groupAlertSummary() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = true + groupAlertBehavior = GROUP_ALERT_SUMMARY + }) { + assertShouldHeadsUp(it) + } + } + + @Test + fun testShouldNotHeadsUp_groupedSummaryNotif_groupAlertChildren() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = true + groupAlertBehavior = GROUP_ALERT_CHILDREN + }) { + assertShouldNotHeadsUp(it) + } + } + + @Test + fun testShouldHeadsUp_ungroupedSummaryNotif_groupAlertChildren() { + withPeekAndPulseEntry({ + isGrouped = false + isGroupSummary = true + groupAlertBehavior = GROUP_ALERT_CHILDREN + }) { + assertShouldHeadsUp(it) + } + } + + @Test + fun testShouldHeadsUp_groupedChildNotif_groupAlertAll() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = false + groupAlertBehavior = GROUP_ALERT_ALL + }) { + assertShouldHeadsUp(it) + } + } + + @Test + fun testShouldHeadsUp_groupedChildNotif_groupAlertChildren() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = false + groupAlertBehavior = GROUP_ALERT_CHILDREN + }) { + assertShouldHeadsUp(it) + } + } + + @Test + fun testShouldNotHeadsUp_groupedChildNotif_groupAlertSummary() { + withPeekAndPulseEntry({ + isGrouped = true + isGroupSummary = false + groupAlertBehavior = GROUP_ALERT_SUMMARY + }) { + assertShouldNotHeadsUp(it) + } + } + + @Test + fun testShouldHeadsUp_ungroupedChildNotif_groupAlertSummary() { + withPeekAndPulseEntry({ + isGrouped = false + isGroupSummary = false + groupAlertBehavior = GROUP_ALERT_SUMMARY + }) { + assertShouldHeadsUp(it) + } + } + @Test - fun testShouldBubble() { + fun testShouldNotHeadsUp_justLaunchedFsi() { + withPeekAndPulseEntry({ hasJustLaunchedFsi = true }) { assertShouldNotHeadsUp(it) } + } + + @Test + fun testShouldBubble_withIntentAndIcon() { ensureBubbleState() - assertShouldBubble(buildBubbleEntry()) + assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = false }) + } + + @Test + fun testShouldBubble_withShortcut() { + ensureBubbleState() + assertShouldBubble(buildBubbleEntry { bubbleIsShortcut = true }) + } + + @Test + fun testShouldNotBubble_notAllowed() { + ensureBubbleState() + assertShouldNotBubble(buildBubbleEntry { canBubble = false }) + } + + @Test + fun testShouldNotBubble_noBubbleMetadata() { + ensureBubbleState() + assertShouldNotBubble(buildBubbleEntry { hasBubbleMetadata = false }) } @Test @@ -339,6 +464,18 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { assertShouldBubble(buildBubbleEntry()) } + @Test + fun testShouldNotAlert_hiddenOnKeyguard() { + ensurePeekState({ keyguardShouldHideNotification = true }) + assertShouldNotHeadsUp(buildPeekEntry()) + + ensurePulseState({ keyguardShouldHideNotification = true }) + assertShouldNotHeadsUp(buildPulseEntry()) + + ensureBubbleState({ keyguardShouldHideNotification = true }) + assertShouldNotBubble(buildBubbleEntry()) + } + @Test fun testShouldFsi_notInteractive() { ensureNotInteractiveFsiState() @@ -357,7 +494,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { assertShouldFsi(buildFsiEntry()) } - private data class State( + protected data class State( var hunSettingEnabled: Boolean? = null, var hunSnoozed: Boolean? = null, var isAodPowerSave: Boolean? = null, @@ -370,7 +507,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var statusBarState: Int? = null, ) - private fun setState(state: State): Unit = + protected fun setState(state: State): Unit = state.run { hunSettingEnabled?.let { val newSetting = if (it) HEADS_UP_ON else HEADS_UP_OFF @@ -401,7 +538,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { statusBarState?.let { statusBarStateController.state = it } } - private fun ensureState(block: State.() -> Unit) = + protected fun ensureState(block: State.() -> Unit) = State() .apply { keyguardShouldHideNotification = false @@ -409,7 +546,7 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } .run(this::setState) - private fun ensurePeekState(block: State.() -> Unit = {}) = ensureState { + protected fun ensurePeekState(block: State.() -> Unit = {}) = ensureState { hunSettingEnabled = true hunSnoozed = false isDozing = false @@ -418,67 +555,67 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { run(block) } - private fun ensurePulseState(block: State.() -> Unit = {}) = ensureState { + protected fun ensurePulseState(block: State.() -> Unit = {}) = ensureState { isAodPowerSave = false isDozing = true pulseOnNotificationsEnabled = true run(block) } - private fun ensureBubbleState(block: State.() -> Unit = {}) = ensureState(block) + protected fun ensureBubbleState(block: State.() -> Unit = {}) = ensureState(block) - private fun ensureNotInteractiveFsiState(block: State.() -> Unit = {}) = ensureState { + protected fun ensureNotInteractiveFsiState(block: State.() -> Unit = {}) = ensureState { isDreaming = false isInteractive = false statusBarState = SHADE run(block) } - private fun ensureDreamingFsiState(block: State.() -> Unit = {}) = ensureState { + protected fun ensureDreamingFsiState(block: State.() -> Unit = {}) = ensureState { isDreaming = true isInteractive = true statusBarState = SHADE run(block) } - private fun ensureKeyguardFsiState(block: State.() -> Unit = {}) = ensureState { + protected fun ensureKeyguardFsiState(block: State.() -> Unit = {}) = ensureState { isDreaming = false isInteractive = true statusBarState = KEYGUARD run(block) } - private fun assertShouldHeadsUp(entry: NotificationEntry) = + protected fun assertShouldHeadsUp(entry: NotificationEntry) = provider.makeUnloggedHeadsUpDecision(entry).let { assertTrue("unexpected suppressed HUN: ${it.logReason}", it.shouldInterrupt) } - private fun assertShouldNotHeadsUp(entry: NotificationEntry) = + protected fun assertShouldNotHeadsUp(entry: NotificationEntry) = provider.makeUnloggedHeadsUpDecision(entry).let { assertFalse("unexpected unsuppressed HUN: ${it.logReason}", it.shouldInterrupt) } - private fun assertShouldBubble(entry: NotificationEntry) = + protected fun assertShouldBubble(entry: NotificationEntry) = provider.makeAndLogBubbleDecision(entry).let { assertTrue("unexpected suppressed bubble: ${it.logReason}", it.shouldInterrupt) } - private fun assertShouldNotBubble(entry: NotificationEntry) = + protected fun assertShouldNotBubble(entry: NotificationEntry) = provider.makeAndLogBubbleDecision(entry).let { assertFalse("unexpected unsuppressed bubble: ${it.logReason}", it.shouldInterrupt) } - private fun assertShouldFsi(entry: NotificationEntry) = + protected fun assertShouldFsi(entry: NotificationEntry) = provider.makeUnloggedFullScreenIntentDecision(entry).let { assertTrue("unexpected suppressed FSI: ${it.logReason}", it.shouldInterrupt) } - private fun assertShouldNotFsi(entry: NotificationEntry) = + protected fun assertShouldNotFsi(entry: NotificationEntry) = provider.makeUnloggedFullScreenIntentDecision(entry).let { assertFalse("unexpected unsuppressed FSI: ${it.logReason}", it.shouldInterrupt) } - private class EntryBuilder(val context: Context) { + protected class EntryBuilder(val context: Context) { var importance = IMPORTANCE_DEFAULT var suppressedVisualEffects: Int? = null var whenMs: Long? = null @@ -487,20 +624,33 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { var canBubble: Boolean? = null var isBubble = false var hasBubbleMetadata = false - var bubbleSuppressNotification: Boolean? = null - - private fun buildBubbleMetadata() = - BubbleMetadata.Builder( - PendingIntent.getActivity( - context, - /* requestCode = */ 0, - Intent().setPackage(context.packageName), - FLAG_MUTABLE - ), - Icon.createWithResource(context.resources, R.drawable.android) - ) - .apply { bubbleSuppressNotification?.let { setSuppressNotification(it) } } - .build() + var bubbleIsShortcut = false + var bubbleSuppressesNotification: Boolean? = null + var isGrouped = false + var isGroupSummary: Boolean? = null + var groupAlertBehavior: Int? = null + var hasJustLaunchedFsi = false + + private fun buildBubbleMetadata(): BubbleMetadata { + val builder = + if (bubbleIsShortcut) { + BubbleMetadata.Builder(context.packageName + ":test_shortcut_id") + } else { + BubbleMetadata.Builder( + PendingIntent.getActivity( + context, + /* requestCode = */ 0, + Intent().setPackage(context.packageName), + FLAG_MUTABLE + ), + Icon.createWithResource(context.resources, R.drawable.android) + ) + } + + bubbleSuppressesNotification?.let { builder.setSuppressNotification(it) } + + return builder.build() + } fun build() = Notification.Builder(context, TEST_CHANNEL_ID) @@ -517,6 +667,14 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { if (hasBubbleMetadata) { setBubbleMetadata(buildBubbleMetadata()) } + + if (isGrouped) { + setGroup(TEST_GROUP_KEY) + } + + isGroupSummary?.let { setGroupSummary(it) } + + groupAlertBehavior?.let { setGroupAlertBehavior(it) } } .build() .apply { @@ -537,6 +695,10 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } .build()!! .also { + if (hasJustLaunchedFsi) { + it.notifyFullScreenIntentLaunched() + } + modifyRanking(it) .apply { suppressedVisualEffects?.let { setSuppressedVisualEffects(it) } @@ -546,27 +708,27 @@ abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() { } } - private fun buildEntry(block: EntryBuilder.() -> Unit) = + protected fun buildEntry(block: EntryBuilder.() -> Unit) = EntryBuilder(context).also(block).build() - private fun buildPeekEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { + protected fun buildPeekEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { importance = IMPORTANCE_HIGH run(block) } - private fun buildPulseEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { + protected fun buildPulseEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { importance = IMPORTANCE_DEFAULT visibilityOverride = VISIBILITY_NO_OVERRIDE run(block) } - private fun buildBubbleEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { + protected fun buildBubbleEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { canBubble = true hasBubbleMetadata = true run(block) } - private fun buildFsiEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { + protected fun buildFsiEntry(block: EntryBuilder.() -> Unit = {}) = buildEntry { importance = IMPORTANCE_HIGH hasFsi = true run(block) @@ -581,3 +743,4 @@ private const val TEST_CHANNEL_ID = "test_channel" private const val TEST_CHANNEL_NAME = "Test Channel" private const val TEST_PACKAGE = "test_package" private const val TEST_TAG = "test_tag" +private const val TEST_GROUP_KEY = "test_group_key" diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index e1c6ddefc13b1775bef67499fdb7289f66317e74..799bb403a8ff251c97b2f61bb185e657c5dc8ae5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -1,8 +1,17 @@ package com.android.systemui.communal.data.repository +import com.android.systemui.communal.shared.model.CommunalSceneKey +import kotlinx.coroutines.flow.MutableStateFlow + /** Fake implementation of [CommunalRepository]. */ -class FakeCommunalRepository : CommunalRepository { - override var isCommunalEnabled = false +class FakeCommunalRepository( + override var isCommunalEnabled: Boolean = false, + override val desiredScene: MutableStateFlow<CommunalSceneKey> = + MutableStateFlow(CommunalSceneKey.Blank) +) : CommunalRepository { + override fun setDesiredScene(desiredScene: CommunalSceneKey) { + this.desiredScene.value = desiredScene + } fun setIsCommunalEnabled(value: Boolean) { isCommunalEnabled = value diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTilePackageUpdatesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTilePackageUpdatesRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f972f52729f6994ba7bb97869867fc25e61115f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTilePackageUpdatesRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.custom.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow + +class FakeCustomTilePackageUpdatesRepository : CustomTilePackageUpdatesRepository { + + private val mutablePackageChanges = MutableSharedFlow<Unit>() + + override val packageChanges: Flow<Unit> + get() = mutablePackageChanges + + suspend fun emitPackageChange() { + mutablePackageChanges.emit(Unit) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index bdddc042009d2c51ceeadc7f7ea754aea93ffec2..6beb513c32d70035f73fed8aaff830850833a959 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -131,6 +131,7 @@ class SceneTestUtils( SceneKey.Lockscreen, SceneKey.Bouncer, SceneKey.Gone, + SceneKey.Communal, ) } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java index cfe2af9763509ba4dc4a0465cb86f60dc3bb91ed..5953d0d309de9203e9591482d55eb900d4011dd7 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java @@ -39,9 +39,12 @@ import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.accessibility.EventStreamTransformation; +import com.android.server.accessibility.Flags; +import com.android.server.accessibility.gestures.GestureMatcher; import com.android.server.accessibility.gestures.MultiTap; import com.android.server.accessibility.gestures.MultiTapAndHold; +import java.util.ArrayList; import java.util.List; /** @@ -453,20 +456,45 @@ public class WindowMagnificationGestureHandler extends MagnificationGestureHandl private final MagnificationGesturesObserver mGesturesObserver; DetectingState(@UiContext Context context) { - final MultiTap multiTap = new MultiTap(context, mDetectSingleFingerTripleTap ? 3 : 1, - mDetectSingleFingerTripleTap - ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP - : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, null); - final MultiTapAndHold multiTapAndHold = new MultiTapAndHold(context, - mDetectSingleFingerTripleTap ? 3 : 1, - mDetectSingleFingerTripleTap - ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD - : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, null); - mGesturesObserver = new MagnificationGesturesObserver(this, - new SimpleSwipe(context), - multiTap, - multiTapAndHold, - new TwoFingersDownOrSwipe(context)); + if (Flags.enableMagnificationMultipleFingerMultipleTapGesture()) { + final List<GestureMatcher> mGestureMatchers = new ArrayList<>(); + + mGestureMatchers.add(new SimpleSwipe(context)); + // Observe single tap and single tap and hold to reduce response time when the + // user performs these two gestures inside the window magnifier. + mGestureMatchers.add(new MultiTap(context, + mDetectSingleFingerTripleTap ? 3 : 1, + mDetectSingleFingerTripleTap + ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP + : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, + null)); + mGestureMatchers.add(new MultiTapAndHold(context, + mDetectSingleFingerTripleTap ? 3 : 1, + mDetectSingleFingerTripleTap + ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD + : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, + null)); + mGestureMatchers.add(new TwoFingersDownOrSwipe(context)); + + mGesturesObserver = new MagnificationGesturesObserver(this, + mGestureMatchers.toArray(new GestureMatcher[mGestureMatchers.size()])); + } else { + final MultiTap multiTap = new MultiTap(context, + mDetectSingleFingerTripleTap ? 3 : 1, + mDetectSingleFingerTripleTap + ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP + : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, null); + final MultiTapAndHold multiTapAndHold = new MultiTapAndHold(context, + mDetectSingleFingerTripleTap ? 3 : 1, + mDetectSingleFingerTripleTap + ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD + : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, null); + mGesturesObserver = new MagnificationGesturesObserver(this, + new SimpleSwipe(context), + multiTap, + multiTapAndHold, + new TwoFingersDownOrSwipe(context)); + } } @Override diff --git a/services/core/Android.bp b/services/core/Android.bp index 7dbf61bab3a9b9065bdee423aaf2ae7ac4891554..a14f3fee5303283c749ab573d000cb0f2986e849 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -183,6 +183,7 @@ java_library_static { "android.hardware.power.stats-V2-java", "android.hidl.manager-V1.2-java", "cbor-java", + "com.android.media.audio-aconfig-java", "dropbox_flags_lib", "icu4j_calendar_astronomer", "android.security.aaid_aidl-java", diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 638abdba36ec670a029b4b0cd027dd1dd5dbe18e..4f322203192facf10a4c0dbce90ecc98f69ca848 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -299,14 +299,6 @@ public abstract class PackageManagerInternal { public abstract String[] setPackagesSuspendedByAdmin( @UserIdInt int userId, @NonNull String[] packageNames, boolean suspended); - /** - * Suspend or unsuspend packages in a profile when quiet mode is toggled. - * - * @param userId The target user. - * @param suspended Whether the packages should be suspended or unsuspended. - */ - public abstract void setPackagesSuspendedForQuietMode(@UserIdInt int userId, boolean suspended); - /** * Get the information describing the dialog to be shown to the user when they try to launch a * suspended application. diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index db89cac2a9431248b253145c3789fc3ca5450d72..c4cb81645e0f55ca2f8f036b442541326a767505 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -179,6 +179,15 @@ option java_package com.android.server 3130 pm_snapshot_stats (build_count|1|1),(reuse_count|1|1),(big_builds|1|1),(short_lived|1|1),(max_build_time|1|3),(cumm_build_time|2|3) # Snapshot rebuild instance 3131 pm_snapshot_rebuild (build_time|1|3),(lifetime|1|3) +# Caller information to clear application data +1003160 pm_clear_app_data_caller (pid|1),(uid|1),(package|3) +# --------------------------- +# Installer.java +# --------------------------- +# Caller Information to clear application data +1003200 installer_clear_app_data_caller (pid|1),(uid|1),(package|3),(flags|1) +# Call stack to clear application data +1003201 installer_clear_app_data_call_stack (method|3),(class|3),(file|3),(line|1) # --------------------------- # InputMethodManagerService.java diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java index 5a15f17a09224631c16c85d526b1c505643e99a9..2b30c013235cd34b05c539c5ff37dfca95daf856 100644 --- a/services/core/java/com/android/server/MasterClearReceiver.java +++ b/services/core/java/com/android/server/MasterClearReceiver.java @@ -88,6 +88,9 @@ public class MasterClearReceiver extends BroadcastReceiver { mWipeEsims = intent.getBooleanExtra(Intent.EXTRA_WIPE_ESIMS, false); final boolean forceWipe = intent.getBooleanExtra(Intent.EXTRA_FORCE_MASTER_CLEAR, false) || intent.getBooleanExtra(Intent.EXTRA_FORCE_FACTORY_RESET, false); + // This is ONLY used by TestHarnessService within System Server, so we don't add a proper + // API constant in Intent for this. + final boolean keepMemtagMode = intent.getBooleanExtra("keep_memtag_mode", false); // TODO(b/189938391): properly handle factory reset on headless system user mode. final int sendingUserId = getSendingUserId(); @@ -110,9 +113,11 @@ public class MasterClearReceiver extends BroadcastReceiver { try { Slog.i(TAG, "Calling RecoverySystem.rebootWipeUserData(context, " + "shutdown=" + shutdown + ", reason=" + reason - + ", forceWipe=" + forceWipe + ", wipeEsims=" + mWipeEsims + ")"); + + ", forceWipe=" + forceWipe + ", wipeEsims=" + mWipeEsims + + ", keepMemtagMode=" + keepMemtagMode + ")"); RecoverySystem - .rebootWipeUserData(context, shutdown, reason, forceWipe, mWipeEsims); + .rebootWipeUserData( + context, shutdown, reason, forceWipe, mWipeEsims, keepMemtagMode); Slog.wtf(TAG, "Still running after master clear?!"); } catch (IOException e) { Slog.e(TAG, "Can't perform master clear/factory reset", e); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 88bb66f8ef228167de0441c718a2c3e42b8e22bf..1566113b7f47167aa8bb65916f9eee86bd62bb06 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1886,7 +1886,7 @@ public class ActivityManagerService extends IActivityManager.Stub int exitInfoReason = (int) args.arg3; args.recycle(); forceStopPackageLocked(pkg, appId, false, false, true, false, - false, userId, reason, exitInfoReason); + false, false, userId, reason, exitInfoReason); } } break; @@ -3548,6 +3548,7 @@ public class ActivityManagerService extends IActivityManager.Stub enforceNotIsolatedCaller("clearApplicationUserData"); int uid = Binder.getCallingUid(); int pid = Binder.getCallingPid(); + EventLog.writeEvent(EventLogTags.AM_CLEAR_APP_DATA_CALLER, pid, uid, packageName); final int resolvedUserId = mUserController.handleIncomingUser(pid, uid, userId, false, ALLOW_FULL_ONLY, "clearApplicationUserData", null); @@ -3914,7 +3915,10 @@ public class ActivityManagerService extends IActivityManager.Stub + packageName + ": " + e); } if (mUserController.isUserRunning(user, userRunningFlags)) { - forceStopPackageLocked(packageName, pkgUid, + forceStopPackageLocked(packageName, UserHandle.getAppId(pkgUid), + false /* callerWillRestart */, false /* purgeCache */, + true /* doIt */, false /* evenPersistent */, + false /* uninstalling */, true /* packageStateStopped */, user, reason == null ? ("from pid " + callingPid) : reason); finishForceStopPackageLocked(packageName, pkgUid); } @@ -4163,7 +4167,7 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") private void forceStopPackageLocked(final String packageName, int uid, String reason) { forceStopPackageLocked(packageName, UserHandle.getAppId(uid), false, - false, true, false, false, UserHandle.getUserId(uid), reason); + false, true, false, false, false, UserHandle.getUserId(uid), reason); } @GuardedBy("this") @@ -4349,20 +4353,20 @@ public class ActivityManagerService extends IActivityManager.Stub @GuardedBy("this") final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, - boolean evenPersistent, boolean uninstalling, int userId, String reasonString) { - + boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, + int userId, String reasonString) { int reason = packageName == null ? ApplicationExitInfo.REASON_USER_STOPPED : ApplicationExitInfo.REASON_USER_REQUESTED; return forceStopPackageLocked(packageName, appId, callerWillRestart, purgeCache, doit, - evenPersistent, uninstalling, userId, reasonString, reason); + evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason); } @GuardedBy("this") final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, - boolean evenPersistent, boolean uninstalling, int userId, String reasonString, - int reason) { + boolean evenPersistent, boolean uninstalling, boolean packageStateStopped, + int userId, String reasonString, int reason) { int i; if (userId == UserHandle.USER_ALL && packageName == null) { @@ -4443,7 +4447,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } - if (packageName == null || uninstalling) { + if (packageName == null || uninstalling || packageStateStopped) { didSomething |= mPendingIntentController.removePendingIntentsForPackage( packageName, userId, appId, doit); } @@ -5148,7 +5152,7 @@ public class ActivityManagerService extends IActivityManager.Stub for (String pkg : pkgs) { synchronized (ActivityManagerService.this) { if (forceStopPackageLocked(pkg, -1, false, false, false, false, false, - 0, "query restart")) { + false, 0, "query restart")) { setResultCode(Activity.RESULT_OK); return; } @@ -7342,7 +7346,7 @@ public class ActivityManagerService extends IActivityManager.Stub mDebugTransient = !persistent; if (packageName != null) { forceStopPackageLocked(packageName, -1, false, false, true, true, - false, UserHandle.USER_ALL, "set debug app"); + false, false, UserHandle.USER_ALL, "set debug app"); } } } finally { @@ -14918,7 +14922,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (list != null && list.length > 0) { for (int i = 0; i < list.length; i++) { forceStopPackageLocked(list[i], -1, false, true, true, - false, false, userId, "storage unmount"); + false, false, false, userId, "storage unmount"); } mAtmInternal.cleanupRecentTasksForUser(UserHandle.USER_ALL); sendPackageBroadcastLocked( @@ -14945,8 +14949,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (killProcess) { forceStopPackageLocked(ssp, UserHandle.getAppId( intent.getIntExtra(Intent.EXTRA_UID, -1)), - false, true, true, false, fullUninstall, userId, - "pkg removed"); + false, true, true, false, fullUninstall, false, + userId, "pkg removed"); getPackageManagerInternal() .onPackageProcessKilledForUninstall(ssp); } else { @@ -15864,7 +15868,7 @@ public class ActivityManagerService extends IActivityManager.Stub } else { // Instrumentation can kill and relaunch even persistent processes forceStopPackageLocked(ii.targetPackage, -1, true, false, true, true, false, - userId, "start instr"); + false, userId, "start instr"); // Inform usage stats to make the target package active if (mUsageStatsService != null) { mUsageStatsService.reportEvent(ii.targetPackage, userId, @@ -15993,6 +15997,7 @@ public class ActivityManagerService extends IActivityManager.Stub /* doIt= */ true, /* evenPersistent= */ true, /* uninstalling= */ false, + /* packageStateStopped= */ false, userId, "start instr"); @@ -16163,8 +16168,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } else if (!instr.mNoRestart) { forceStopPackageLocked(app.info.packageName, -1, false, false, true, true, false, - app.userId, - "finished inst"); + false, app.userId, "finished inst"); } } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index 9e9db6aff699def2cbe9068ed6ff35e84dd5f387..931914f17729358be229dca6fe514b1219d6238b 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -129,3 +129,6 @@ option java_package com.android.server.am # Intent Sender redirect for UserHandle.USER_CURRENT 30110 am_intent_sender_redirect_user (userId|1|5) + +# Caller information to clear application data +1030002 am_clear_app_data_caller (pid|1),(uid|1),(package|3) diff --git a/services/core/java/com/android/server/am/LmkdStatsReporter.java b/services/core/java/com/android/server/am/LmkdStatsReporter.java index 1e4dd648cab5e0ec4feedb097ac0284d9d37ffb3..507fd9efaffda569c0b8109cfc3c130ebcb231e7 100644 --- a/services/core/java/com/android/server/am/LmkdStatsReporter.java +++ b/services/core/java/com/android/server/am/LmkdStatsReporter.java @@ -107,6 +107,8 @@ public final class LmkdStatsReporter { return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__LOW_MEM_AND_SWAP_UTIL; case LOW_FILECACHE_AFTER_THRASHING: return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__LOW_FILECACHE_AFTER_THRASHING; + case LOW_MEM: + return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__LOW_MEM; default: return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__UNKNOWN; } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 7c0797020f0369db6eedc267bb7a715283f9b588..2efac12a53a2c6795526b6379b6d0998895abfa8 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2040,7 +2040,7 @@ public final class ProcessList { // the package was initially frozen through KILL_APPLICATION_MSG, so // it doesn't hurt to use it again.) mService.forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), - false, false, true, false, false, app.userId, "start failure"); + false, false, true, false, false, false, app.userId, "start failure"); return false; } } @@ -2115,7 +2115,7 @@ public final class ProcessList { + app.processName, e); app.setPendingStart(false); mService.forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), - false, false, true, false, false, app.userId, "start failure"); + false, false, true, false, false, false, app.userId, "start failure"); } return app.getPid() > 0; } @@ -2148,7 +2148,7 @@ public final class ProcessList { app.setPendingStart(false); mService.forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), - false, false, true, false, false, app.userId, "start failure"); + false, false, true, false, false, false, app.userId, "start failure"); } } }; diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index c1a2482249ef4fa06f4520ee170a5072f3bb9d14..e6cdbb58a9fda69b855d96a7550832932caf007a 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -128,6 +128,7 @@ public class SettingsToPropertiesMapper { "biometrics", "biometrics_framework", "biometrics_integration", + "camera_hal", "camera_platform", "car_framework", "car_perception", diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index c5dd01fb8791b0f90e52a8bddea5fb83e4f6c9da..87633e9e255df8a3bb3663fbd0d807b44ac2a688 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -76,7 +76,6 @@ import android.app.BroadcastOptions; import android.app.IStopUserCallback; import android.app.IUserSwitchObserver; import android.app.KeyguardManager; -import android.app.admin.DevicePolicyManagerInternal; import android.app.usage.UsageEvents; import android.appwidget.AppWidgetManagerInternal; import android.content.Context; @@ -1497,10 +1496,8 @@ class UserController implements Handler.Callback { private boolean shouldStartWithParent(UserInfo user) { final UserProperties properties = getUserProperties(user.id); - DevicePolicyManagerInternal dpmi = - LocalServices.getService(DevicePolicyManagerInternal.class); return (properties != null && properties.getStartWithParent()) - && (!user.isQuietModeEnabled() || dpmi.isKeepProfilesRunningEnabled()); + && !user.isQuietModeEnabled(); } /** @@ -3629,7 +3626,7 @@ class UserController implements Handler.Callback { void activityManagerForceStopPackage(@UserIdInt int userId, String reason) { synchronized (mService) { - mService.forceStopPackageLocked(null, -1, false, false, true, false, false, + mService.forceStopPackageLocked(null, -1, false, false, true, false, false, false, userId, reason); } }; diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index cbaf05bb4e231e788bcbd88b9bf70219b8d09de4..a770b66b250694b1c1df3b26681920fd2d118851 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -22,3 +22,10 @@ flag { description: "Detect abusive FGS behavior for certain types (camera, mic, media, location)." bug: "295545575" } + +flag { + name: "fgs_boot_completed" + namespace: "backstage_power" + description: "Disable BOOT_COMPLETED broadcast FGS start for certain types" + bug: "296558535" +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index a8fa313abc7a21cc20576ba309cc915e4014b158..b209fb00626849dbe5971baff3a874687ed4ff1d 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -37,6 +37,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE; import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; +import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization; import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE; import static com.android.server.utils.EventLogger.Event.ALOGE; import static com.android.server.utils.EventLogger.Event.ALOGI; @@ -203,7 +204,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; -import com.android.media.audio.flags.Flags; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -10509,7 +10509,7 @@ public class AudioService extends IAudioService.Stub } private boolean isBluetoothPrividged() { - if (!Flags.bluetoothMacAddressAnonymization()) { + if (!bluetoothMacAddressAnonymization()) { return true; } return PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( diff --git a/services/core/java/com/android/server/audio/HardeningEnforcer.java b/services/core/java/com/android/server/audio/HardeningEnforcer.java index c7556dacb78306d559fc036087f40bae99774e34..4ceb83b2e1c9af74171bb4f4454f326557e55f40 100644 --- a/services/core/java/com/android/server/audio/HardeningEnforcer.java +++ b/services/core/java/com/android/server/audio/HardeningEnforcer.java @@ -15,7 +15,7 @@ */ package com.android.server.audio; -import static com.android.media.audio.flags.Flags.autoPublicVolumeApiHardening; +import static android.media.audio.Flags.autoPublicVolumeApiHardening; import android.Manifest; import android.content.Context; diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 4538cad513d621fff861c8e2e708d94cb5a80379..0629e6373b6eda08fd0619f8933a5b235828cc62 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -39,6 +39,7 @@ import android.content.pm.PackageManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.ComponentInfoInternal; +import android.hardware.biometrics.Flags; import android.hardware.biometrics.IAuthService; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; @@ -332,6 +333,33 @@ public class AuthService extends SystemService { } } + @Override + public long getLastAuthenticationTime(int userId, + @Authenticators.Types int authenticators) throws RemoteException { + // Only allow internal clients to call getLastAuthenticationTime with a different + // userId. + final int callingUserId = UserHandle.getCallingUserId(); + + if (userId != callingUserId) { + checkInternalPermission(); + } else { + checkPermission(); + } + + final long identity = Binder.clearCallingIdentity(); + try { + // We can't do this above because we need the READ_DEVICE_CONFIG permission, which + // the calling user may not possess. + if (!Flags.lastAuthenticationTime()) { + throw new UnsupportedOperationException(); + } + + return mBiometricService.getLastAuthenticationTime(userId, authenticators); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public boolean hasEnrolledBiometrics(int userId, String opPackageName) throws RemoteException { diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 1898b8015462faab658ed8ede878d22c702ad11a..91a68ea67b3b9d5402747889571f2289e88bb8f0 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.hardware.biometrics.BiometricManager.BIOMETRIC_NO_AUTHENTICATION; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; @@ -39,6 +40,7 @@ import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -53,6 +55,7 @@ import android.hardware.biometrics.SensorPropertiesInternal; import android.hardware.camera2.CameraManager; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.security.keymint.HardwareAuthenticatorType; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -62,10 +65,16 @@ import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.ServiceSpecificException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.security.Authorization; +import android.security.GateKeeper; import android.security.KeyStore; +import android.security.authorization.IKeystoreAuthorization; +import android.security.authorization.ResponseCode; +import android.service.gatekeeper.IGateKeeperService; import android.text.TextUtils; import android.util.ArraySet; import android.util.Pair; @@ -79,6 +88,7 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.internal.util.DumpUtils; import com.android.server.SystemService; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.utils.Slogf; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -116,6 +126,10 @@ public class BiometricService extends SystemService { KeyStore mKeyStore; @VisibleForTesting ITrustManager mTrustManager; + @VisibleForTesting + IKeystoreAuthorization mKeystoreAuthorization; + @VisibleForTesting + IGateKeeperService mGateKeeper; // Get and cache the available biometric authenticators and their associated info. final ArrayList<BiometricSensor> mSensors = new ArrayList<>(); @@ -615,6 +629,64 @@ public class BiometricService extends SystemService { } } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override // Binder call + public long getLastAuthenticationTime( + int userId, @Authenticators.Types int authenticators) { + super.getLastAuthenticationTime_enforcePermission(); + + if (!Flags.lastAuthenticationTime()) { + throw new UnsupportedOperationException(); + } + + Slogf.d(TAG, "getLastAuthenticationTime(userId=%d, authenticators=0x%x)", + userId, authenticators); + + final long secureUserId; + try { + secureUserId = mGateKeeper.getSecureUserId(userId); + } catch (RemoteException e) { + Slogf.w(TAG, "Failed to get secure user id for " + userId, e); + return BIOMETRIC_NO_AUTHENTICATION; + } + + if (secureUserId == GateKeeper.INVALID_SECURE_USER_ID) { + Slogf.w(TAG, "No secure user id for " + userId); + return BIOMETRIC_NO_AUTHENTICATION; + } + + ArrayList<Integer> hardwareAuthenticators = new ArrayList<>(2); + + if ((authenticators & Authenticators.DEVICE_CREDENTIAL) != 0) { + hardwareAuthenticators.add(HardwareAuthenticatorType.PASSWORD); + } + + if ((authenticators & Authenticators.BIOMETRIC_STRONG) != 0) { + hardwareAuthenticators.add(HardwareAuthenticatorType.FINGERPRINT); + } + + if (hardwareAuthenticators.isEmpty()) { + throw new IllegalArgumentException("authenticators must not be empty"); + } + + int[] authTypesArray = hardwareAuthenticators.stream() + .mapToInt(Integer::intValue) + .toArray(); + try { + return mKeystoreAuthorization.getLastAuthTime(secureUserId, authTypesArray); + } catch (RemoteException e) { + Slog.w(TAG, "Error getting last auth time: " + e); + return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; + } catch (ServiceSpecificException e) { + // This is returned when the feature flag test fails in keystore2 + if (e.errorCode == ResponseCode.PERMISSION_DENIED) { + throw new UnsupportedOperationException(); + } + + return BiometricConstants.BIOMETRIC_NO_AUTHENTICATION; + } + } + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) @Override public boolean hasEnrolledBiometrics(int userId, String opPackageName) { @@ -937,6 +1009,14 @@ public class BiometricService extends SystemService { return ActivityManager.getService(); } + public IKeystoreAuthorization getKeystoreAuthorizationService() { + return Authorization.getService(); + } + + public IGateKeeperService getGateKeeperService() { + return GateKeeper.getService(); + } + public ITrustManager getTrustManager() { return ITrustManager.Stub.asInterface(ServiceManager.getService(Context.TRUST_SERVICE)); } @@ -1050,6 +1130,8 @@ public class BiometricService extends SystemService { mBiometricContext = injector.getBiometricContext(context); mUserManager = injector.getUserManager(context); mBiometricCameraManager = injector.getBiometricCameraManager(context); + mKeystoreAuthorization = injector.getKeystoreAuthorizationService(); + mGateKeeper = injector.getGateKeeperService(); try { injector.getActivityManagerService().registerUserSwitchObserver( diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index e475fe63746d49e19984b43d0cd0c22b07acf69a..ff12ca2de766f5ca252fb56ffe75d0590b46b6a1 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -102,6 +102,7 @@ import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionManager; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; @@ -239,6 +240,10 @@ public final class DisplayManagerService extends SystemService { private static final String FORCE_WIFI_DISPLAY_ENABLE = "persist.debug.wfd.enable"; private static final String PROP_DEFAULT_DISPLAY_TOP_INSET = "persist.sys.displayinset.top"; + + @VisibleForTesting + static final String ENABLE_ON_CONNECT = + "persist.sys.display.enable_on_connect.external"; private static final long WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT = 10000; // This value needs to be in sync with the threshold // in RefreshRateConfigs::getFrameRateDivisor. @@ -1530,8 +1535,8 @@ public final class DisplayManagerService extends SystemService { throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or " + "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate " + "MediaProjection token in order to create a screen sharing virtual " - + "display. In order to create a virtual display that does not perform" - + "screen sharing (mirroring), please use the flag" + + "display. In order to create a virtual display that does not perform " + + "screen sharing (mirroring), please use the flag " + "VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY."); } } @@ -1942,10 +1947,14 @@ public final class DisplayManagerService extends SystemService { } setupLogicalDisplay(display); - // TODO(b/292196201) Remove when the display can be disabled before DPC is created. if (display.getDisplayInfoLocked().type == Display.TYPE_EXTERNAL) { - display.setEnabledLocked(false); + if ((Build.IS_ENG || Build.IS_USERDEBUG) + && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) { + Slog.w(TAG, "External display is enabled by default, bypassing user consent."); + } else { + display.setEnabledLocked(false); + } } sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED); diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 3cf0786b7e2877bf98234a4938bc8f35fe3f2e05..330818ed17ca779539025b1d3e5019ed50baada8 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.media.IMediaRoute2ProviderService; import android.media.IMediaRoute2ProviderServiceCallback; +import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; import android.media.RouteDiscoveryPreference; @@ -641,6 +642,15 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider @Override public void notifyProviderUpdated(MediaRoute2ProviderInfo providerInfo) { + for (MediaRoute2Info route : providerInfo.getRoutes()) { + if (route.isSystemRoute()) { + throw new SecurityException( + "Only the system is allowed to publish system routes. " + + "Disallowed route: " + + route); + } + } + Connection connection = mConnectionRef.get(); if (connection != null) { connection.postProviderUpdated(providerInfo); diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index b2f00a246c23035dffd3fd226f6737c9669f2548..1bdd402cf0b566f90d045dc95850bf89e41f0b03 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -128,12 +128,12 @@ import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; +import static com.android.server.notification.Flags.expireBitmaps; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; import static com.android.server.utils.PriorityDump.PRIORITY_ARG; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_CRITICAL; import static com.android.server.utils.PriorityDump.PRIORITY_ARG_NORMAL; -import static com.android.server.notification.Flags.expireBitmaps; import android.Manifest; import android.Manifest.permission; @@ -178,7 +178,6 @@ import android.app.compat.CompatChanges; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.app.usage.UsageEvents; -import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; import android.companion.ICompanionDeviceManager; import android.compat.annotation.ChangeId; @@ -206,8 +205,6 @@ import android.content.pm.UserInfo; import android.content.pm.VersionedPackage; import android.content.res.Resources; import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.graphics.drawable.Icon; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.AudioManagerInternal; @@ -320,7 +317,6 @@ import com.android.server.SystemService; import com.android.server.job.JobSchedulerInternal; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; -import com.android.server.notification.Flags; import com.android.server.notification.ManagedServices.ManagedServiceInfo; import com.android.server.notification.ManagedServices.UserProfiles; import com.android.server.notification.toast.CustomToastRecord; @@ -1935,7 +1931,7 @@ public class NotificationManagerService extends SystemService { } else if ( isProfileUnavailable(action)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); - if (userHandle >= 0 && !mDpm.isKeepProfilesRunningEnabled()) { + if (userHandle >= 0) { cancelAllNotificationsInt(MY_UID, MY_PID, null, null, 0, 0, userHandle, REASON_PROFILE_TURNED_OFF); mSnoozeHelper.clearData(userHandle); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 2951ef630eb390369c580ccc7ecb3e08e4e02b64..a52870e17b1616909f6e693e01395aacb6d5190c 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -112,6 +112,7 @@ import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ArchivedPackageParcel; import android.content.pm.DataLoaderType; +import android.content.pm.Flags; import android.content.pm.PackageInfo; import android.content.pm.PackageInfoLite; import android.content.pm.PackageInstaller; @@ -159,6 +160,7 @@ import com.android.internal.util.CollectionUtils; import com.android.server.EventLogTags; import com.android.server.LocalManagerRegistry; import com.android.server.SystemConfig; +import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; import com.android.server.pm.Installer.LegacyDexoptDisabledException; @@ -2524,8 +2526,15 @@ final class InstallPackageHelper { LocalManagerRegistry.getManager(PackageManagerLocal.class); try (PackageManagerLocal.FilteredSnapshot snapshot = packageManagerLocal.withFilteredSnapshot()) { - DexoptParams params = - dexoptOptions.convertToDexoptParams(0 /* extraFlags */); + boolean ignoreDexoptProfile = + (installRequest.getInstallFlags() + & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE) + != 0; + /*@DexoptFlags*/ int extraFlags = + ignoreDexoptProfile && Flags.useArtServiceV2() + ? ArtFlags.FLAG_IGNORE_PROFILE + : 0; + DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags); DexoptResult dexOptResult = DexOptHelper.getArtManagerLocal().dexoptPackage( snapshot, packageName, params); installRequest.onDexoptFinished(dexOptResult); diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 4ed31636ad56e415ff88ccc42f9fedd73cb6ca74..d5471cb015270207e97bf13e8dca729c4f253433 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageStats; +import android.os.Binder; import android.os.Build; import android.os.CreateAppDataArgs; import android.os.CreateAppDataResult; @@ -35,9 +36,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.storage.CrateMetadata; import android.text.format.DateUtils; +import android.util.EventLog; import android.util.Slog; import com.android.internal.os.BackgroundThread; +import com.android.server.EventLogTags; import com.android.server.SystemService; import dalvik.system.BlockGuard; @@ -441,6 +444,26 @@ public class Installer extends SystemService { if (!checkBeforeRemote()) return; try { mInstalld.clearAppData(uuid, packageName, userId, flags, ceDataInode); + + final StackTraceElement[] elements = Thread.currentThread().getStackTrace(); + String className; + String methodName; + String fileName; + int lineNumber; + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + EventLog.writeEvent(EventLogTags.INSTALLER_CLEAR_APP_DATA_CALLER, pid, uid, packageName, + flags); + // Skip the first two elements since they are always the same, ie + // Thread#getStackTrace() and VMStack#getThreadStackTrace() + for (int i = 2; i < elements.length; i++) { + className = elements[i].getClassName(); + methodName = elements[i].getMethodName(); + fileName = elements[i].getFileName(); + lineNumber = elements[i].getLineNumber(); + EventLog.writeEvent(EventLogTags.INSTALLER_CLEAR_APP_DATA_CALL_STACK, methodName, + className, fileName, lineNumber); + } } catch (Exception e) { throw InstallerException.from(e); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 305e3536ad25666a7522563f193e3370a1cf754e..a4d8632ac0775db82010881ec435b1ee77f65831 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -58,6 +58,7 @@ import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.VersionedPackage; +import android.content.pm.parsing.FrameworkParsingPackageUtils; import android.graphics.Bitmap; import android.net.Uri; import android.os.Binder; @@ -666,17 +667,22 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements // App package name and label length is restricted so that really long strings aren't // written to disk. - if (params.appPackageName != null - && params.appPackageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) { + if (params.appPackageName != null && !isValidPackageName(params.appPackageName)) { params.appPackageName = null; } params.appLabel = TextUtils.trimToSize(params.appLabel, PackageItemInfo.MAX_SAFE_LABEL_LENGTH); - String requestedInstallerPackageName = (params.installerPackageName != null - && params.installerPackageName.length() < SessionParams.MAX_PACKAGE_NAME_LENGTH) - ? params.installerPackageName : installerPackageName; + // Validate installer package name. + if (params.installerPackageName != null && !isValidPackageName( + params.installerPackageName)) { + params.installerPackageName = null; + } + + var requestedInstallerPackageName = + params.installerPackageName != null ? params.installerPackageName + : installerPackageName; if (PackageManagerServiceUtils.isRootOrShell(callingUid) || PackageInstallerSession.isSystemDataLoaderInstallation(params) @@ -1105,6 +1111,19 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements return Integer.parseInt(sessionId); } + private static boolean isValidPackageName(@NonNull String packageName) { + if (packageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) { + return false; + } + // "android" is a valid package name + var errorMessage = FrameworkParsingPackageUtils.validateName( + packageName, /* requireSeparator= */ false, /* requireFilename */ true); + if (errorMessage != null) { + return false; + } + return true; + } + private File getTmpSessionDir(String volumeUuid) { return Environment.getDataAppDirectory(volumeUuid); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 638bcbe5822cdd6e20319dbab6a94eaa9e36d309..e5f7962e254c6e28cf6e1aaba2dc71c48adaca09 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2085,8 +2085,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mUserNeedsBadging, () -> mResolveInfo, () -> mInstantAppInstallerActivity, injector.getBackgroundHandler()); mDexOptHelper = new DexOptHelper(this); - mSuspendPackageHelper = new SuspendPackageHelper(this, mInjector, mUserManager, - mBroadcastHelper, mProtectedPackages); + mSuspendPackageHelper = new SuspendPackageHelper(this, mInjector, mBroadcastHelper, + mProtectedPackages); mDistractingPackageHelper = new DistractingPackageHelper(this, mBroadcastHelper, mSuspendPackageHelper); mStorageEventHelper = new StorageEventHelper(this, mDeletePackageHelper, @@ -4658,6 +4658,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService throw new SecurityException("Cannot clear data for a protected package: " + packageName); } + final int callingPid = Binder.getCallingPid(); + EventLog.writeEvent(EventLogTags.PM_CLEAR_APP_DATA_CALLER, callingPid, callingUid, + packageName); // Queue up an async operation since the package deletion may take a little while. mHandler.post(new Runnable() { @@ -4791,6 +4794,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService /* checkShell= */ false, "delete application cache files"); final int hasAccessInstantApps = mContext.checkCallingOrSelfPermission( android.Manifest.permission.ACCESS_INSTANT_APPS); + final int callingPid = Binder.getCallingPid(); + EventLog.writeEvent(EventLogTags.PM_CLEAR_APP_DATA_CALLER, callingPid, callingUid, + packageName); // Queue up an async operation since the package deletion may take a little while. mHandler.post(() -> { @@ -6145,7 +6151,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended, appExtras, launcherExtras, dialogInfo, callingPackage, userId, callingUid, - false /* forQuietMode */, quarantined); + quarantined); } @Override @@ -6601,12 +6607,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService snapshotComputer(), userId, packageNames, suspended); } - @Override - public void setPackagesSuspendedForQuietMode(int userId, boolean suspended) { - mSuspendPackageHelper.setPackagesSuspendedForQuietMode( - snapshotComputer(), userId, suspended); - } - @Override public void setDeviceAndProfileOwnerPackages( int deviceOwnerUserId, String deviceOwnerPackage, diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 7264e2eff4aa61aab218faf3989bca486033ccaf..d4abad8499f26827a3a733b71b57017829220361 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -3703,6 +3703,9 @@ class PackageManagerShellCommand extends ShellCommand { sessionParams.installFlags |= PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; break; + case "--ignore-dexopt-profile": + sessionParams.installFlags |= PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE; + break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -4799,7 +4802,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--enable-rollback]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); pw.println(" [--apex] [--non-staged] [--force-non-staged]"); - pw.println(" [--staged-ready-timeout TIMEOUT]"); + pw.println(" [--staged-ready-timeout TIMEOUT] [--ignore-dexopt-profile]"); pw.println(" [PATH [SPLIT...]|-]"); pw.println(" Install an application. Must provide the apk data to install, either as"); pw.println(" file path(s) or '-' to read from stdin. Options are:"); @@ -4839,6 +4842,13 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" milliseconds for pre-reboot verification to complete when"); pw.println(" performing staged install. This flag is used to alter the waiting"); pw.println(" time. You can skip the waiting time by specifying a TIMEOUT of '0'"); + pw.println(" --ignore-dexopt-profile: If set, all profiles are ignored by dexopt"); + pw.println(" during the installation, including the profile in the DM file and"); + pw.println(" the profile embedded in the APK file. If an invalid profile is"); + pw.println(" provided during installation, no warning will be reported by `adb"); + pw.println(" install`."); + pw.println(" This option does not affect later dexopt operations (e.g.,"); + pw.println(" background dexopt and manual `pm compile` invocations)."); pw.println(""); pw.println(" install-existing [--user USER_ID|all|current]"); pw.println(" [--instant] [--full] [--wait] [--restrict-permissions] PACKAGE"); diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java index e8cebefb863171ef8c6c3209cd5b811d07a87a9f..71f6c0d507d4fdf0a1ccd04255f60aeb248cecb7 100644 --- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java +++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java @@ -16,8 +16,6 @@ package com.android.server.pm; -import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; -import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.os.Process.SYSTEM_UID; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -27,9 +25,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.AppOpsManager; -import android.app.admin.DevicePolicyManagerInternal; import android.content.Intent; -import android.content.pm.PackageInfo; import android.content.pm.SuspendDialogInfo; import android.os.Binder; import android.os.Bundle; @@ -45,7 +41,6 @@ import android.util.Slog; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; -import com.android.server.LocalServices; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.PackageUserStateInternal; @@ -54,10 +49,8 @@ import com.android.server.pm.pkg.mutate.PackageUserStateWrite; import com.android.server.utils.WatchedArrayMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.function.Predicate; public final class SuspendPackageHelper { @@ -68,7 +61,6 @@ public final class SuspendPackageHelper { private final PackageManagerService mPm; private final PackageManagerServiceInjector mInjector; - private final UserManagerService mUserManager; private final BroadcastHelper mBroadcastHelper; private final ProtectedPackages mProtectedPackages; @@ -76,10 +68,8 @@ public final class SuspendPackageHelper { * Constructor for {@link PackageManagerService}. */ SuspendPackageHelper(PackageManagerService pm, PackageManagerServiceInjector injector, - UserManagerService userManager, BroadcastHelper broadcastHelper, - ProtectedPackages protectedPackages) { + BroadcastHelper broadcastHelper, ProtectedPackages protectedPackages) { mPm = pm; - mUserManager = userManager; mInjector = injector; mBroadcastHelper = broadcastHelper; mProtectedPackages = protectedPackages; @@ -102,7 +92,6 @@ public final class SuspendPackageHelper { * @param callingPackage The caller's package name. * @param userId The user where packages reside. * @param callingUid The caller's uid. - * @param forQuietMode Whether suspension is for quiet mode, in which case no apps are exempt. * @return The names of failed packages. */ @Nullable @@ -110,11 +99,11 @@ public final class SuspendPackageHelper { boolean suspended, @Nullable PersistableBundle appExtras, @Nullable PersistableBundle launcherExtras, @Nullable SuspendDialogInfo dialogInfo, @NonNull String callingPackage, @UserIdInt int userId, int callingUid, - boolean forQuietMode, boolean quarantined) { + boolean quarantined) { if (ArrayUtils.isEmpty(packageNames)) { return packageNames; } - if (suspended && !quarantined && !forQuietMode && !isSuspendAllowedForUser(snapshot, userId, + if (suspended && !quarantined && !isSuspendAllowedForUser(snapshot, userId, callingUid)) { Slog.w(TAG, "Cannot suspend due to restrictions on user " + userId); return packageNames; @@ -130,7 +119,7 @@ public final class SuspendPackageHelper { final ArraySet<String> changedPackagesList = new ArraySet<>(packageNames.length); final IntArray changedUids = new IntArray(packageNames.length); - final boolean[] canSuspend = suspended && !forQuietMode + final boolean[] canSuspend = suspended ? canSuspendPackageForUser(snapshot, packageNames, userId, callingUid) : null; for (int i = 0; i < packageNames.length; i++) { @@ -620,92 +609,10 @@ public final class SuspendPackageHelper { */ public String[] setPackagesSuspendedByAdmin( Computer snapshot, int userId, String[] packageNames, boolean suspend) { - final Set<String> toSuspend = new ArraySet<>(packageNames); - List<String> unsuspendable = new ArrayList<>(); - - if (mUserManager.isQuietModeEnabled(userId)) { - // If the user is in quiet mode, most apps will already be suspended, we shouldn't - // re-suspend or unsuspend them. - final Set<String> quiet = packagesToSuspendInQuietMode(snapshot, userId); - quiet.retainAll(toSuspend); - if (!quiet.isEmpty()) { - Slog.i(TAG, "Ignoring quiet packages: " + String.join(", ", quiet)); - toSuspend.removeAll(quiet); - } - - // Some of the already suspended packages might not be suspendable by the admin - // (e.g. current dialer package), we need to report it back as unsuspendable the same - // way as if quiet mode wasn't enabled. In that latter case they'd be returned by - // setPackagesSuspended below after unsuccessful attempt to suspend them. - if (suspend) { - unsuspendable = getUnsuspendablePackages(snapshot, userId, quiet); - } - } - if (!toSuspend.isEmpty()) { - unsuspendable.addAll(Arrays.asList( - setPackagesSuspended( - snapshot, toSuspend.toArray(new String[0]), suspend, - null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, - PackageManagerService.PLATFORM_PACKAGE_NAME, userId, Process.SYSTEM_UID, - false /* forQuietMode */, false /* quarantined */))); - } - return unsuspendable.toArray(String[]::new); - } - - private List<String> getUnsuspendablePackages( - Computer snapshot, int userId, Set<String> packages) { - final String[] toSuspendArray = packages.toArray(String[]::new); - final boolean[] mask = - canSuspendPackageForUser(snapshot, toSuspendArray, userId, Process.SYSTEM_UID); - final List<String> result = new ArrayList<>(); - for (int i = 0; i < mask.length; i++) { - if (!mask[i]) { - result.add(toSuspendArray[i]); - } - } - return result; - } - - /** - * Suspends or unsuspends all packages in the given user when quiet mode is toggled to prevent - * usage while quiet mode is enabled. - */ - public void setPackagesSuspendedForQuietMode( - Computer snapshot, int userId, boolean suspend) { - final Set<String> toSuspend = packagesToSuspendInQuietMode(snapshot, userId); - if (!suspend) { - final DevicePolicyManagerInternal dpm = - LocalServices.getService(DevicePolicyManagerInternal.class); - if (dpm != null) { - toSuspend.removeAll(dpm.getPackagesSuspendedByAdmin(userId)); - } else { - Slog.wtf(TAG, - "DevicePolicyManager unavailable while suspending apps for quiet mode"); - } - } - - if (toSuspend.isEmpty()) { - return; - } - - setPackagesSuspended(snapshot, toSuspend.toArray(new String[0]), - suspend, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, + return setPackagesSuspended(snapshot, packageNames, suspend, + null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, PackageManagerService.PLATFORM_PACKAGE_NAME, userId, Process.SYSTEM_UID, - true /* forQuietMode */, false /* quarantined */); - } - - private Set<String> packagesToSuspendInQuietMode(Computer snapshot, int userId) { - final List<PackageInfo> pkgInfos = snapshot.getInstalledPackages( - MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, userId).getList(); - final Set<String> result = new ArraySet<>(); - for (PackageInfo info : pkgInfos) { - result.add(info.packageName); - } - - // Role holder may be null, but ArraySet handles it correctly. - result.remove(mPm.getDevicePolicyManagementRoleHolderPackageName(userId)); - - return result; + false /* quarantined */); } private String getKnownPackageName(@NonNull Computer snapshot, diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c97fbdad9bf40c87fd34404921ea42d16ee9efc4..81a570f0e7a5919e3fd408326e463ff4c2fe41f2 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -46,7 +46,6 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityManagerNative; -import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.IActivityManager; import android.app.IStopUserCallback; @@ -55,7 +54,6 @@ import android.app.PendingIntent; import android.app.StatsManager; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManagerInternal; -import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.IIntentReceiver; @@ -302,19 +300,6 @@ public class UserManagerService extends IUserManager.Stub { private static final String TRON_USER_CREATED = "users_user_created"; private static final String TRON_DEMO_CREATED = "users_demo_created"; - // App ops that should be restricted in quiet mode - private static final int[] QUIET_MODE_RESTRICTED_APP_OPS = { - AppOpsManager.OP_COARSE_LOCATION, - AppOpsManager.OP_FINE_LOCATION, - AppOpsManager.OP_GPS, - AppOpsManager.OP_BODY_SENSORS, - AppOpsManager.OP_ACTIVITY_RECOGNITION, - AppOpsManager.OP_BLUETOOTH_SCAN, - AppOpsManager.OP_NEARBY_WIFI_DEVICES, - AppOpsManager.OP_RECORD_AUDIO, - AppOpsManager.OP_CAMERA, - }; - private final Context mContext; private final PackageManagerService mPm; @@ -339,7 +324,6 @@ public class UserManagerService extends IUserManager.Stub { private final File mUserListFile; private final IBinder mUserRestrictionToken = new Binder(); - private final IBinder mQuietModeToken = new Binder(); /** Installs system packages based on user-type. */ private final UserSystemPackageInstaller mSystemPackageInstaller; @@ -702,7 +686,6 @@ public class UserManagerService extends IUserManager.Stub { @Override public void onUserStarting(@NonNull TargetUser targetUser) { - boolean isProfileInQuietMode = false; synchronized (mUms.mUsersLock) { final UserData user = mUms.getUserDataLU(targetUser.getUserIdentifier()); if (user != null) { @@ -710,14 +693,9 @@ public class UserManagerService extends IUserManager.Stub { if (targetUser.getUserIdentifier() == UserHandle.USER_SYSTEM && targetUser.isFull()) { mUms.setLastEnteredForegroundTimeToNow(user); - } else if (user.info.isManagedProfile() && user.info.isQuietModeEnabled()) { - isProfileInQuietMode = true; } } } - if (isProfileInQuietMode) { - mUms.setAppOpsRestrictedForQuietMode(targetUser.getUserIdentifier(), true); - } } @Override @@ -1516,43 +1494,21 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mPackagesLock) { writeUserLP(profileUserData); } - if (getDevicePolicyManagerInternal().isKeepProfilesRunningEnabled()) { - // New behavior: when quiet mode is enabled, profile user is running, but apps are - // suspended. - getPackageManagerInternal().setPackagesSuspendedForQuietMode(userId, enableQuietMode); - setAppOpsRestrictedForQuietMode(userId, enableQuietMode); - if (enableQuietMode - && !mLockPatternUtils.isManagedProfileWithUnifiedChallenge(userId)) { - mContext.getSystemService(TrustManager.class).setDeviceLockedForUser(userId, true); - } - - if (!enableQuietMode && target != null) { - try { - mContext.startIntentSender(target, null, 0, 0, 0); - } catch (IntentSender.SendIntentException e) { - Slog.e(LOG_TAG, "Failed to start intent after disabling quiet mode", e); - } - } - } else { - // Old behavior: when quiet is enabled, profile user is stopped. - // Old quiet mode behavior: profile user is stopped. - // TODO(b/265683382) Remove once rollout complete. - try { - if (enableQuietMode) { - ActivityManager.getService().stopUser(userId, /* force= */ true, null); - LocalServices.getService(ActivityManagerInternal.class) - .killForegroundAppsForUser(userId); - } else { - IProgressListener callback = target != null - ? new DisableQuietModeUserUnlockedCallback(target) - : null; - ActivityManager.getService().startProfileWithListener(userId, callback); - } - } catch (RemoteException e) { - // Should not happen, same process. - e.rethrowAsRuntimeException(); + try { + if (enableQuietMode) { + ActivityManager.getService().stopUser(userId, /* force= */ true, null); + LocalServices.getService(ActivityManagerInternal.class) + .killForegroundAppsForUser(userId); + } else { + IProgressListener callback = target != null + ? new DisableQuietModeUserUnlockedCallback(target) + : null; + ActivityManager.getService().startProfileWithListener(userId, callback); } + } catch (RemoteException e) { + // Should not happen, same process. + e.rethrowAsRuntimeException(); } logQuietModeEnabled(userId, enableQuietMode, callingPackage); @@ -1569,17 +1525,6 @@ public class UserManagerService extends IUserManager.Stub { } } - private void setAppOpsRestrictedForQuietMode(@UserIdInt int userId, boolean restrict) { - for (int opCode : QUIET_MODE_RESTRICTED_APP_OPS) { - try { - mAppOpsService.setUserRestriction( - opCode, restrict, mQuietModeToken, userId, /* excludedPackageTags= */ null); - } catch (RemoteException e) { - Slog.w(LOG_TAG, "Unable to limit app ops", e); - } - } - } - private void logQuietModeEnabled(@UserIdInt int userId, boolean enableQuietMode, @Nullable String callingPackage) { Slogf.i(LOG_TAG, diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 2ad8bcf4739e9cb88c14075db2ef3f21a98b92c6..9e20805c287400d914297ab83092fb096ec941a1 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -379,10 +379,7 @@ public class PackageInfoUtils { ai.privateFlags |= flag(state.isInstantApp(), ApplicationInfo.PRIVATE_FLAG_INSTANT) | flag(state.isVirtualPreload(), ApplicationInfo.PRIVATE_FLAG_VIRTUAL_PRELOAD) | flag(state.isHidden(), ApplicationInfo.PRIVATE_FLAG_HIDDEN); - if ((flags & PackageManager.MATCH_QUARANTINED_COMPONENTS) == 0 - && state.isQuarantined()) { - ai.enabled = false; - } else if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { ai.enabled = true; } else if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index a6558e07b2aa13be3e3e70aada2675725564911b..eea13f179b9eccd5b113fc4baf695d32686d2bb0 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -14575,9 +14575,10 @@ public class BatteryStatsImpl extends BatteryStats { mModStepMode = 0; } } else { - if (level >= 90) { - // If the battery level is at least 90%, always consider the device to be - // charging even if it happens to go down a level. + if (level >= mConstants.BATTERY_CHARGING_ENFORCE_LEVEL) { + // If the battery level is at least Constants.BATTERY_CHARGING_ENFORCE_LEVEL, + // always consider the device to be charging even if it happens to go down a + // level. changed |= setChargingLocked(true); } else if (!mCharging) { if (mLastChargeStepLevel < level) { @@ -15313,6 +15314,8 @@ public class BatteryStatsImpl extends BatteryStats { public static final String KEY_MAX_HISTORY_BUFFER_KB = "max_history_buffer_kb"; public static final String KEY_BATTERY_CHARGED_DELAY_MS = "battery_charged_delay_ms"; + public static final String KEY_BATTERY_CHARGING_ENFORCE_LEVEL = + "battery_charging_enforce_level"; public static final String KEY_PER_UID_MODEM_POWER_MODEL = "per_uid_modem_power_model"; public static final String KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION = @@ -15363,6 +15366,7 @@ public class BatteryStatsImpl extends BatteryStats { private static final int DEFAULT_MAX_HISTORY_FILES_LOW_RAM_DEVICE = 64; private static final int DEFAULT_MAX_HISTORY_BUFFER_LOW_RAM_DEVICE_KB = 64; /*Kilo Bytes*/ private static final int DEFAULT_BATTERY_CHARGED_DELAY_MS = 900000; /* 15 min */ + private static final int DEFAULT_BATTERY_CHARGING_ENFORCE_LEVEL = 90; @PerUidModemPowerModel private static final int DEFAULT_PER_UID_MODEM_MODEL = PER_UID_MODEM_POWER_MODEL_MODEM_ACTIVITY_INFO_RX_TX; @@ -15384,6 +15388,7 @@ public class BatteryStatsImpl extends BatteryStats { public int MAX_HISTORY_FILES; public int MAX_HISTORY_BUFFER; /*Bytes*/ public int BATTERY_CHARGED_DELAY_MS = DEFAULT_BATTERY_CHARGED_DELAY_MS; + public int BATTERY_CHARGING_ENFORCE_LEVEL = DEFAULT_BATTERY_CHARGING_ENFORCE_LEVEL; public int PER_UID_MODEM_MODEL = DEFAULT_PER_UID_MODEM_MODEL; public boolean PHONE_ON_EXTERNAL_STATS_COLLECTION = DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION; @@ -15412,6 +15417,9 @@ public class BatteryStatsImpl extends BatteryStats { mResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.BATTERY_CHARGING_STATE_UPDATE_DELAY), false /* notifyForDescendants */, this); + mResolver.registerContentObserver(Settings.Global.getUriFor( + Settings.Global.BATTERY_CHARGING_STATE_ENFORCE_LEVEL), + false /* notifyForDescendants */, this); updateConstants(); } @@ -15424,6 +15432,12 @@ public class BatteryStatsImpl extends BatteryStats { updateBatteryChargedDelayMsLocked(); } return; + } else if (uri.equals(Settings.Global.getUriFor( + Settings.Global.BATTERY_CHARGING_STATE_ENFORCE_LEVEL))) { + synchronized (BatteryStatsImpl.this) { + updateBatteryChargingEnforceLevelLocked(); + } + return; } updateConstants(); } @@ -15477,6 +15491,7 @@ public class BatteryStatsImpl extends BatteryStats { DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); updateBatteryChargedDelayMsLocked(); + updateBatteryChargingEnforceLevelLocked(); onChange(); } @@ -15507,6 +15522,21 @@ public class BatteryStatsImpl extends BatteryStats { } } + private void updateBatteryChargingEnforceLevelLocked() { + int lastChargingEnforceLevel = BATTERY_CHARGING_ENFORCE_LEVEL; + final int level = Settings.Global.getInt(mResolver, + Settings.Global.BATTERY_CHARGING_STATE_ENFORCE_LEVEL, + -1); + + BATTERY_CHARGING_ENFORCE_LEVEL = level >= 0 ? level : mParser.getInt( + KEY_BATTERY_CHARGING_ENFORCE_LEVEL, DEFAULT_BATTERY_CHARGING_ENFORCE_LEVEL); + + if (BATTERY_CHARGING_ENFORCE_LEVEL <= mLastChargeStepLevel + && mLastChargeStepLevel < lastChargingEnforceLevel) { + setChargingLocked(true); + } + } + private void updateKernelUidReadersThrottleTime(long oldTimeMs, long newTimeMs) { KERNEL_UID_READERS_THROTTLE_TIME = newTimeMs; if (oldTimeMs != newTimeMs) { @@ -15541,6 +15571,8 @@ public class BatteryStatsImpl extends BatteryStats { pw.println(MAX_HISTORY_BUFFER/1024); pw.print(KEY_BATTERY_CHARGED_DELAY_MS); pw.print("="); pw.println(BATTERY_CHARGED_DELAY_MS); + pw.print(KEY_BATTERY_CHARGING_ENFORCE_LEVEL); pw.print("="); + pw.println(BATTERY_CHARGING_ENFORCE_LEVEL); pw.print(KEY_PER_UID_MODEM_POWER_MODEL); pw.print("="); pw.println(getPerUidModemModelName(PER_UID_MODEM_MODEL)); pw.print(KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION); pw.print("="); diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index 4ebd402863c5b6b6d0a56fcdf26587838ae58ac6..5fd787a1f88c4f5a5bdd858bb8c65f4d78d17f8f 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -126,10 +126,10 @@ public final class StorageSessionController { connection = new StorageUserConnection(mContext, userId, this); mConnections.put(userId, connection); } - Slog.i(TAG, "Creating and starting session with id: " + sessionId); - connection.startSession(sessionId, deviceFd, vol.getPath().getPath(), - vol.getInternalPath().getPath()); } + Slog.i(TAG, "Creating and starting session with id: " + sessionId); + connection.startSession(sessionId, deviceFd, vol.getPath().getPath(), + vol.getInternalPath().getPath()); } /** diff --git a/services/core/java/com/android/server/testharness/TestHarnessModeService.java b/services/core/java/com/android/server/testharness/TestHarnessModeService.java index 9a9b836028322c99dd719801beacb876c1c07332..1f884baf13a151086bc2b13017406b0b615d48df 100644 --- a/services/core/java/com/android/server/testharness/TestHarnessModeService.java +++ b/services/core/java/com/android/server/testharness/TestHarnessModeService.java @@ -71,6 +71,7 @@ import java.util.Set; public class TestHarnessModeService extends SystemService { public static final String TEST_HARNESS_MODE_PROPERTY = "persist.sys.test_harness"; private static final String TAG = TestHarnessModeService.class.getSimpleName(); + private boolean mEnableKeepMemtagMode = false; private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal; @@ -298,6 +299,18 @@ public class TestHarnessModeService extends SystemService { switch (cmd) { case "enable": case "restore": + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "--keep-memtag": + mEnableKeepMemtagMode = true; + break; + default: + getErrPrintWriter().println("Invalid option: " + opt); + return 1; + } + } + checkPermissions(); final long originalId = Binder.clearCallingIdentity(); try { @@ -357,6 +370,7 @@ public class TestHarnessModeService extends SystemService { i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); i.putExtra(Intent.EXTRA_REASON, TAG); i.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true); + i.putExtra("keep_memtag_mode", mEnableKeepMemtagMode); getContext().sendBroadcastAsUser(i, UserHandle.SYSTEM); return 0; } diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index 25c42b4858a402a48b0b6de9689c441e407c43af..f9d344bd7e31e4a557fe47faad2596d8c35b43b3 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -285,10 +285,6 @@ class ActivityStartInterceptor { return false; } - if (isKeepProfilesRunningEnabled() && !isPackageSuspended()) { - return false; - } - IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT); @@ -521,12 +517,6 @@ class ActivityStartInterceptor { && (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) != 0; } - private static boolean isKeepProfilesRunningEnabled() { - DevicePolicyManagerInternal dpmi = - LocalServices.getService(DevicePolicyManagerInternal.class); - return dpmi == null || dpmi.isKeepProfilesRunningEnabled(); - } - /** * Called when an activity is successfully launched. */ diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index c02178506052ab06b216e10e6b74d354f0883707..f462efc00ce686ac28c6c86eedc64da3d7db71a1 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -69,6 +69,7 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; + import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_DREAM; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS; @@ -3825,8 +3826,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution, - boolean takeSnapshotIfNeeded) { + public TaskSnapshot getTaskSnapshot(int taskId, boolean isLowResolution) { mAmInternal.enforceCallingPermission(READ_FRAME_BUFFER, "getTaskSnapshot()"); final long ident = Binder.clearCallingIdentity(); try { @@ -3840,12 +3840,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } } // Don't call this while holding the lock as this operation might hit the disk. - TaskSnapshot taskSnapshot = mWindowManager.mTaskSnapshotController.getSnapshot(taskId, + return mWindowManager.mTaskSnapshotController.getSnapshot(taskId, task.mUserId, true /* restoreFromDisk */, isLowResolution); - if (taskSnapshot == null && takeSnapshotIfNeeded) { - taskSnapshot = takeTaskSnapshot(taskId, false /* updateCache */); - } - return taskSnapshot; } finally { Binder.restoreCallingIdentity(ident); } @@ -7064,8 +7060,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public TaskSnapshot getTaskSnapshotBlocking( int taskId, boolean isLowResolution) { - return ActivityTaskManagerService.this.getTaskSnapshot(taskId, isLowResolution, - false /* takeSnapshotIfNeeded */); + return ActivityTaskManagerService.this.getTaskSnapshot(taskId, isLowResolution); } @Override diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 9f3e16279428b6aadd67826c134feb44e2f1ba54..668cd871415cc333a4e3cae71fdc9cff065f9cf9 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -28,6 +28,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; +import static com.android.window.flags.Flags.balShowToasts; import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -459,6 +460,7 @@ public class BackgroundActivityStartController { "With Android 15 BAL hardening this activity start would be blocked" + " (missing opt in by PI creator)! " + state.dump(resultForCaller, resultForRealCaller)); + showBalToast("BAL would be blocked", state); // return the realCaller result for backwards compatibility return statsLog(resultForRealCaller, state); } @@ -470,6 +472,7 @@ public class BackgroundActivityStartController { "With Android 15 BAL hardening this activity start would be blocked" + " (missing opt in by PI creator)! " + state.dump(resultForCaller, resultForRealCaller)); + showBalToast("BAL would be blocked", state); return statsLog(resultForCaller, state); } if (resultForRealCaller.allows() @@ -481,6 +484,7 @@ public class BackgroundActivityStartController { "With Android 14 BAL hardening this activity start would be blocked" + " (missing opt in by PI sender)! " + state.dump(resultForCaller, resultForRealCaller)); + showBalToast("BAL would be blocked", state); return statsLog(resultForRealCaller, state); } Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed" @@ -488,6 +492,7 @@ public class BackgroundActivityStartController { + state.dump(resultForCaller, resultForRealCaller)); // fall through } + showBalToast("BAL blocked", state); // anything that has fallen through would currently be aborted Slog.w(TAG, "Background activity launch blocked" + state.dump(resultForCaller, resultForRealCaller)); @@ -862,8 +867,7 @@ public class BackgroundActivityStartController { + (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ") + getApplicationLabel(mService.mContext.getPackageManager(), launchedFromPackageName); - UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, - toastText, Toast.LENGTH_LONG).show()); + showToast(toastText); Slog.i(TAG, asmDebugInfo); } @@ -882,6 +886,19 @@ public class BackgroundActivityStartController { return true; } + private void showBalToast(String toastText, BalState state) { + if (balShowToasts()) { + showToast(toastText + + " caller:" + state.mCallingPackage + + " realCaller:" + state.mRealCallingPackage); + } + } + + private void showToast(String toastText) { + UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, + toastText, Toast.LENGTH_LONG).show()); + } + /** * If the top activity uid does not match the launching or launched activity, and the launch was * not requested from the top uid, we want to clear out all non matching activities to prevent @@ -930,12 +947,10 @@ public class BackgroundActivityStartController { if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid) && (!shouldBlockActivityStart || finishCount[0] > 0)) { - UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, - (shouldBlockActivityStart - ? "Top activities cleared by " - : "Top activities would be cleared by ") - + ActivitySecurityModelFeatureFlags.DOC_LINK, - Toast.LENGTH_LONG).show()); + showToast((shouldBlockActivityStart + ? "Top activities cleared by " + : "Top activities would be cleared by ") + + ActivitySecurityModelFeatureFlags.DOC_LINK); Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord, targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart, @@ -1013,11 +1028,10 @@ public class BackgroundActivityStartController { } if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) { - UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, - (ActivitySecurityModelFeatureFlags.DOC_LINK - + (restrictActivitySwitch ? " returned home due to " - : " would return home due to ") - + callingLabel), Toast.LENGTH_LONG).show()); + showToast((ActivitySecurityModelFeatureFlags.DOC_LINK + + (restrictActivitySwitch ? " returned home due to " + : " would return home due to ") + + callingLabel)); } // If the activity switch should be restricted, return home rather than the diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 275396f459bde229ac15bb2a02676039212a2b4e..14628782eac738798cd05036569617980c7fb5e3 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -150,8 +150,8 @@ class EmbeddedWindowController { /** * A unique token associated with the embedded window that can be used by the host window - * to request focus transfer to the embedded. This is not the input token since we don't - * want to give clients access to each others input token. + * to request focus transfer and gesture transfer to the embedded. This is not the input + * token since we don't want to give clients access to each others input token. */ private final IBinder mInputTransferToken; diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index a756847d59eae4536a4f5d395c65bf1dc82c2c0d..0c55d8a85df1508e31b5cc49adb591bd8b0b9dae 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -963,6 +963,23 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { return didTransfer; } + @Override + public boolean transferHostTouchGestureToEmbedded(IWindow hostWindow, + IBinder inputTransferToken) { + if (hostWindow == null) { + return false; + } + + final long identity = Binder.clearCallingIdentity(); + boolean didTransfer; + try { + didTransfer = mService.transferHostTouchGestureToEmbedded(this, hostWindow, + inputTransferToken); + } finally { + Binder.restoreCallingIdentity(identity); + } + return didTransfer; + } @Override public void generateDisplayHash(IWindow window, Rect boundsInWindow, String hashAlgorithm, RemoteCallback callback) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 5c5a1e1d23dc4b2d411c5d9fa423151f4ce65aa8..f3489281da0681e890960bbf12b5ff457a16b8c5 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3508,10 +3508,13 @@ class Task extends TaskFragment { top.mLetterboxUiController.getLetterboxPositionForVerticalReachability(); } } - // User Aspect Ratio Settings is enabled if the app is not in SCM + // User Aspect Ratio Settings button is enabled if the app is not in SCM and has + // launchable activities info.topActivityEligibleForUserAspectRatioButton = top != null && !info.topActivityInSizeCompat - && top.mLetterboxUiController.shouldEnableUserAspectRatioSettings(); + && top.mLetterboxUiController.shouldEnableUserAspectRatioSettings() + && mAtmService.mContext.getPackageManager() + .getLaunchIntentForPackage(getBasePackageName()) != null; info.topActivityBoundsLetterboxed = top != null && top.areBoundsLetterboxed(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9eb3389ff43ccee76dcf79f06a26e3e0d0d2ea60..6e3d24b9f7113d5dd5c6c1b728d234e99f964429 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8960,6 +8960,43 @@ public class WindowManagerService extends IWindowManager.Stub } } + boolean transferHostTouchGestureToEmbedded(Session session, IWindow hostWindow, + IBinder inputTransferToken) { + final IBinder hostInputChannel, embeddedInputChannel; + synchronized (mGlobalLock) { + final WindowState hostWindowState = windowForClientLocked(session, hostWindow, false); + if (hostWindowState == null) { + Slog.w(TAG, "Attempt to transfer touch gesture with invalid host window"); + return false; + } + + final EmbeddedWindowController.EmbeddedWindow ew = + mEmbeddedWindowController.getByInputTransferToken(inputTransferToken); + if (ew == null || ew.mHostWindowState == null) { + Slog.w(TAG, "Attempt to transfer touch gesture to non-existent embedded window"); + return false; + } + if (ew.mHostWindowState.mClient.asBinder() != hostWindow.asBinder()) { + Slog.w(TAG, "Attempt to transfer touch gesture to embedded window not associated" + + " with host window"); + return false; + } + embeddedInputChannel = ew.getInputChannelToken(); + if (embeddedInputChannel == null) { + Slog.w(TAG, "Attempt to transfer touch focus from embedded window with no input" + + " channel"); + return false; + } + hostInputChannel = hostWindowState.mInputChannelToken; + if (hostInputChannel == null) { + Slog.w(TAG, + "Attempt to transfer touch focus to a host window with no input channel"); + return false; + } + return mInputManager.transferTouchFocus(hostInputChannel, embeddedInputChannel); + } + } + private void updateInputChannel(IBinder channelToken, int callingUid, int callingPid, int displayId, SurfaceControl surface, String name, InputApplicationHandle applicationHandle, int flags, diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 176bc28389e573fcf05143566f0dd6c80b12d765..a7d77304d046317e48f3b0dbc78c3009f126f41f 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -86,6 +86,8 @@ namespace input_flags = com::android::input::flags; namespace android { +static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer(); + // The exponent used to calculate the pointer speed scaling factor. // The scaling factor is calculated as 2 ^ (speed * exponent), // where the speed ranges from -7 to + 7 and is supplied by the user. @@ -327,6 +329,8 @@ public: TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr); void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override; bool isInputMethodConnectionActive() override; + std::optional<DisplayViewport> getPointerViewportForAssociatedDisplay( + int32_t associatedDisplayId) override; /* --- InputDispatcherPolicyInterface implementation --- */ @@ -374,8 +378,10 @@ public: virtual PointerIconStyle getCustomPointerIconId(); virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position); - /* --- PointerControllerPolicyInterface implementation --- */ - std::shared_ptr<PointerControllerInterface> createPointerController() override; + /* --- PointerChoreographerPolicyInterface implementation --- */ + std::shared_ptr<PointerControllerInterface> createPointerController( + PointerControllerInterface::ControllerType type) override; + void notifyPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override; private: sp<InputManagerInterface> mInputManager; @@ -492,7 +498,9 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "Pointer Capture: %s, seq=%" PRIu32 "\n", mLocked.pointerCaptureRequest.enable ? "Enabled" : "Disabled", mLocked.pointerCaptureRequest.seq); - forEachPointerControllerLocked([&dump](PointerController& pc) { dump += pc.dump(); }); + if (auto pc = mLocked.legacyPointerController.lock(); pc) { + dump += pc->dump(); + } } // release lock dump += "\n"; @@ -537,6 +545,9 @@ void NativeInputManager::setDisplayViewports(JNIEnv* env, jobjectArray viewportO [&viewports](PointerController& pc) { pc.onDisplayViewportsUpdated(viewports); }); } // release lock + if (ENABLE_POINTER_CHOREOGRAPHER) { + mInputManager->getChoreographer().setDisplayViewports(viewports); + } mInputManager->getReader().requestRefreshConfiguration( InputReaderConfiguration::Change::DISPLAY_INFO); } @@ -721,6 +732,7 @@ void NativeInputManager::forEachPointerControllerLocked( continue; } apply(*pc); + it++; } } @@ -735,9 +747,6 @@ std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerCon if (controller == nullptr) { ensureSpriteControllerLocked(); - static const bool ENABLE_POINTER_CHOREOGRAPHER = - input_flags::enable_pointer_choreographer(); - // Disable the functionality of the legacy PointerController if PointerChoreographer is // enabled. controller = PointerController::create(this, mLooper, *mLocked.spriteController, @@ -749,17 +758,43 @@ std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerCon return controller; } -std::shared_ptr<PointerControllerInterface> NativeInputManager::createPointerController() { +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); + PointerController::create(this, mLooper, *mLocked.spriteController, /*enabled=*/true, + 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. + { // acquire lock + std::scoped_lock _l(mLock); + if (mLocked.pointerDisplayId == pointerDisplayId) { + return; + } + mLocked.pointerDisplayId = pointerDisplayId; + ALOGI("%s: pointer displayId set to: %d", __func__, pointerDisplayId); + } // release lock + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::DISPLAY_INFO); + + // Notify the system. JNIEnv* env = jniEnv(); env->CallVoidMethod(mServiceObj, gServiceClassInfo.onPointerDisplayIdChanged, pointerDisplayId, position.x, position.y); @@ -1118,19 +1153,23 @@ void NativeInputManager::updateInactivityTimeoutLocked() REQUIRES(mLock) { } void NativeInputManager::setPointerDisplayId(int32_t displayId) { - { // acquire lock - std::scoped_lock _l(mLock); + if (ENABLE_POINTER_CHOREOGRAPHER) { + mInputManager->getChoreographer().setDefaultMouseDisplayId(displayId); + } else { + { // acquire lock + std::scoped_lock _l(mLock); - if (mLocked.pointerDisplayId == displayId) { - return; - } + if (mLocked.pointerDisplayId == displayId) { + return; + } - ALOGI("Setting pointer display id to %d.", displayId); - mLocked.pointerDisplayId = displayId; - } // release lock + ALOGI("Setting pointer display id to %d.", displayId); + mLocked.pointerDisplayId = displayId; + } // release lock - mInputManager->getReader().requestRefreshConfiguration( - InputReaderConfiguration::Change::DISPLAY_INFO); + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::DISPLAY_INFO); + } } void NativeInputManager::setPointerSpeed(int32_t speed) { @@ -1356,6 +1395,11 @@ bool NativeInputManager::isInputMethodConnectionActive() { return result; } +std::optional<DisplayViewport> NativeInputManager::getPointerViewportForAssociatedDisplay( + int32_t associatedDisplayId) { + return mInputManager->getChoreographer().getViewportForPointerDevice(associatedDisplayId); +} + bool NativeInputManager::filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) { ATRACE_CALL(); JNIEnv* env = jniEnv(); @@ -1689,6 +1733,9 @@ void NativeInputManager::setStylusButtonMotionEventsEnabled(bool enabled) { } FloatPoint NativeInputManager::getMouseCursorPosition() { + if (ENABLE_POINTER_CHOREOGRAPHER) { + return mInputManager->getChoreographer().getMouseCursorPosition(ADISPLAY_ID_NONE); + } std::scoped_lock _l(mLock); const auto pc = mLocked.legacyPointerController.lock(); if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION}; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java index d9604395dd98f7b3ed7ee8fc1dc59881aa86d66a..395ea9176877027cda670dbec88af82e8ff1445d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyData.java @@ -16,8 +16,6 @@ package com.android.server.devicepolicy; -import static com.android.server.devicepolicy.DevicePolicyManagerService.DEFAULT_KEEP_PROFILES_RUNNING_FLAG; - import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -200,7 +198,7 @@ class DevicePolicyData { * Effective state of the feature flag. It is updated to the current configuration value * during boot and doesn't change value after than unless overridden by test code. */ - boolean mEffectiveKeepProfilesRunning = DEFAULT_KEEP_PROFILES_RUNNING_FLAG; + boolean mEffectiveKeepProfilesRunning = false; DevicePolicyData(@UserIdInt int userId) { mUserId = userId; @@ -401,7 +399,7 @@ class DevicePolicyData { out.endTag(null, TAG_BYPASS_ROLE_QUALIFICATIONS); } - if (policyData.mEffectiveKeepProfilesRunning != DEFAULT_KEEP_PROFILES_RUNNING_FLAG) { + if (policyData.mEffectiveKeepProfilesRunning) { out.startTag(null, TAG_KEEP_PROFILES_RUNNING); out.attributeBoolean(null, ATTR_VALUE, policyData.mEffectiveKeepProfilesRunning); out.endTag(null, TAG_KEEP_PROFILES_RUNNING); @@ -592,7 +590,7 @@ class DevicePolicyData { policy.mCurrentRoleHolder = parser.getAttributeValue(null, ATTR_VALUE); } else if (TAG_KEEP_PROFILES_RUNNING.equals(tag)) { policy.mEffectiveKeepProfilesRunning = parser.getAttributeBoolean( - null, ATTR_VALUE, DEFAULT_KEEP_PROFILES_RUNNING_FLAG); + null, ATTR_VALUE, false); // Deprecated tags below } else if (TAG_PROTECTED_PACKAGES.equals(tag)) { if (policy.mUserControlDisabledPackages == null) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 1ff117e2c60bae5a3da479ba799fa5a0de84a058..93dc2190c8a3abec4ea58cdda477c2aabf8d3de8 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -873,9 +873,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { "enable_permission_based_access"; private static final boolean DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG = false; - // TODO(b/265683382) remove the flag after rollout. - public static final boolean DEFAULT_KEEP_PROFILES_RUNNING_FLAG = false; - // TODO(b/266831522) remove the flag after rollout. private static final String APPLICATION_EXEMPTIONS_FLAG = "application_exemptions"; private static final boolean DEFAULT_APPLICATION_EXEMPTIONS_FLAG = true; @@ -2178,13 +2175,29 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return packageNameAndSignature; } - private void suspendAppsForQuietProfiles(boolean toSuspend) { + private void unsuspendAppsForQuietProfiles() { PackageManagerInternal pmi = mInjector.getPackageManagerInternal(); List<UserInfo> users = mUserManagerInternal.getUsers(true /* excludeDying */); + for (UserInfo user : users) { - if (user.isManagedProfile() && user.isQuietModeEnabled()) { - pmi.setPackagesSuspendedForQuietMode(user.id, toSuspend); + if (!user.isManagedProfile() || !user.isQuietModeEnabled()) { + continue; } + int userId = user.id; + var suspendedByAdmin = getPackagesSuspendedByAdmin(userId); + var packagesToUnsuspend = mInjector.getPackageManager(userId) + .getInstalledPackages(PackageManager.PackageInfoFlags.of( + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE)) + .stream() + .map(packageInfo -> packageInfo.packageName) + .filter(pkg -> !suspendedByAdmin.contains(pkg)) + .toArray(String[]::new); + + Slogf.i(LOG_TAG, "Unsuspending work apps for user %d", userId); + // When app suspension was used for quiet mode, the apps were suspended by platform + // package, just like when admin suspends them. So although it wasn't admin who + // suspended, this method will remove the right suspension record. + pmi.setPackagesSuspendedByAdmin(userId, packagesToUnsuspend, false /* suspended */); } } @@ -3436,9 +3449,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - // In case flag value has changed, we apply it during boot to avoid doing it concurrently - // with user toggling quiet mode. - setKeepProfileRunningEnabledUnchecked(isKeepProfilesRunningFlagEnabled()); + // Check whether work apps were paused via suspension and unsuspend if necessary. + // TODO: move it into PolicyVersionUpgrader so that it is executed only once. + unsuspendWorkAppsIfNecessary(); } // TODO(b/230841522) Make it static. @@ -11039,9 +11052,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { (size == 1 ? "" : "s")); } pw.println(); - pw.println("Keep profiles running: " - + getUserData(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning); - pw.println(); mPolicyCache.dump(pw); pw.println(); @@ -15538,11 +15548,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { packageName, findInteractAcrossProfilesResetMode(packageName), userId); } - @Override - public Set<String> getPackagesSuspendedByAdmin(@UserIdInt int userId) { - return DevicePolicyManagerService.this.getPackagesSuspendedByAdmin(userId); - } - @Override public void notifyUnsafeOperationStateChanged(DevicePolicySafetyChecker checker, int reason, boolean isSafe) { @@ -15571,11 +15576,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } - @Override - public boolean isKeepProfilesRunningEnabled() { - return getUserDataUnchecked(UserHandle.USER_SYSTEM).mEffectiveKeepProfilesRunning; - } - private @Mode int findInteractAcrossProfilesResetMode(String packageName) { return getDefaultCrossProfilePackages().contains(packageName) ? AppOpsManager.MODE_ALLOWED @@ -23028,32 +23028,22 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG); } - private static boolean isKeepProfilesRunningFlagEnabled() { - return DEFAULT_KEEP_PROFILES_RUNNING_FLAG; - } - private boolean isUnicornFlagEnabled() { return false; } - private void setKeepProfileRunningEnabledUnchecked(boolean keepProfileRunning) { + private void unsuspendWorkAppsIfNecessary() { synchronized (getLockObject()) { DevicePolicyData policyData = getUserDataUnchecked(UserHandle.USER_SYSTEM); - if (policyData.mEffectiveKeepProfilesRunning == keepProfileRunning) { + if (!policyData.mEffectiveKeepProfilesRunning) { return; } - policyData.mEffectiveKeepProfilesRunning = keepProfileRunning; + policyData.mEffectiveKeepProfilesRunning = false; saveSettingsLocked(UserHandle.USER_SYSTEM); } - suspendAppsForQuietProfiles(keepProfileRunning); - } - @Override - public void setOverrideKeepProfilesRunning(boolean enabled) { - Preconditions.checkCallAuthorization( - hasCallingOrSelfPermission(MANAGE_PROFILE_AND_DEVICE_OWNERS)); - setKeepProfileRunningEnabledUnchecked(enabled); - Slog.i(LOG_TAG, "Keep profiles running overridden to: " + enabled); + Slog.w(LOG_TAG, "Work apps may have been paused via suspension previously."); + unsuspendAppsForQuietProfiles(); } public void setMtePolicy(int flags, String callerPackageName) { diff --git a/services/midi/Android.bp b/services/midi/Android.bp index a385fe38495b4ff92da27d5c4a999b3114cb8b0e..2ea28a313706ec7fd5ccd827b6645b31aedcba9c 100644 --- a/services/midi/Android.bp +++ b/services/midi/Android.bp @@ -20,6 +20,5 @@ java_library_static { srcs: [":services.midi-sources"], libs: [ "services.core", - "aconfig_midi_flags_java_lib", ], } diff --git a/services/midi/OWNERS b/services/midi/OWNERS index f4d51f91b51b943ef2b573e50de88c117baf6d17..683cae1b0f3a73cc85315ee53cdef87a2370db46 100644 --- a/services/midi/OWNERS +++ b/services/midi/OWNERS @@ -1 +1,3 @@ philburk@google.com +robertwu@google.com +elaurent@google.com #{LAST_RESORT_SUGGESTION} diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index 2f47cc7160b7f5b274710c20fe92b0ae25e1279e..39aaab25d7be220240ebbafc6c64545a00a2ccb4 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -16,7 +16,7 @@ package com.android.server.midi; -import static com.android.media.midi.flags.Flags.virtualUmp; +import static android.media.midi.Flags.virtualUmp; import android.Manifest; import android.annotation.NonNull; diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt index 93530cf5b0a9876ec6c3ea8020f5aab8b0b9c0a8..acaec211440da2833775bf0dc6bb7a59f942e9c4 100644 --- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt +++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt @@ -16,7 +16,6 @@ package com.android.server.permission.access -import android.app.admin.DevicePolicyManagerInternal import android.content.Context import android.content.pm.PackageManager import android.content.pm.PackageManagerInternal @@ -75,7 +74,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { val userIds = MutableIntSet(userManagerService.userIdsIncludingPreCreated) val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages val isLeanback = systemConfig.isLeanback val configPermissions = systemConfig.permissions val privilegedPermissionAllowlistPackages = @@ -152,7 +151,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { isSystemUpdated: Boolean ) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onStorageVolumeMounted( @@ -169,7 +168,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onPackageAdded(packageName: String) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onPackageAdded( @@ -184,7 +183,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onPackageRemoved(packageName: String, appId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onPackageRemoved( @@ -200,7 +199,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onPackageInstalled(packageName: String, userId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onPackageInstalled( @@ -216,7 +215,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onPackageUninstalled(packageName: String, appId: Int, userId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onPackageUninstalled( @@ -232,69 +231,50 @@ class AccessCheckingService(context: Context) : SystemService(context) { } internal fun onSystemReady() { - val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates - val knownPackages = packageManagerInternal.getKnownPackages(packageStates) - mutateState { - with(policy) { - onSystemReady(packageStates, disabledSystemPackageStates, knownPackages) - } - } + mutateState { with(policy) { onSystemReady() } } } private val PackageManagerLocal.allPackageStates: Pair<Map<String, PackageState>, Map<String, PackageState>> get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates } - private fun PackageManagerInternal.getKnownPackages( - packageStates: Map<String, PackageState> - ): IntMap<Array<String>> = - MutableIntMap<Array<String>>().apply { - this[KnownPackages.PACKAGE_INSTALLER] = - getKnownPackageNames(KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = - getKnownPackageNames( - KnownPackages.PACKAGE_PERMISSION_CONTROLLER, - UserHandle.USER_SYSTEM + private val PackageManagerInternal.knownPackages: IntMap<Array<String>> + get() = + MutableIntMap<Array<String>>().apply { + this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames( + KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM ) - this[KnownPackages.PACKAGE_VERIFIER] = - getKnownPackageNames(KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_SETUP_WIZARD] = - getKnownPackageNames(KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = - getKnownPackageNames( - KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, - UserHandle.USER_SYSTEM + this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames( + KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM ) - this[KnownPackages.PACKAGE_CONFIGURATOR] = - getKnownPackageNames(KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = - getKnownPackageNames( - KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, - UserHandle.USER_SYSTEM + this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames( + KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM ) - this[KnownPackages.PACKAGE_APP_PREDICTOR] = - getKnownPackageNames(KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_COMPANION] = - getKnownPackageNames(KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM) - this[KnownPackages.PACKAGE_RETAIL_DEMO] = - getKnownPackageNames(KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM) - .filter { isProfileOwner(it, packageStates) } - .toTypedArray() - this[KnownPackages.PACKAGE_RECENTS] = - getKnownPackageNames(KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM) - } - - private fun isProfileOwner( - packageName: String, - packageStates: Map<String, PackageState> - ): Boolean { - val appId = packageStates[packageName]?.appId ?: return false - val devicePolicyManagerInternal = - LocalServices.getService(DevicePolicyManagerInternal::class.java) ?: return false - // TODO(b/169395065): Figure out if this flow makes sense in Device Owner mode. - return devicePolicyManagerInternal.isActiveProfileOwner(appId) || - devicePolicyManagerInternal.isActiveDeviceOwner(appId) - } + this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames( + KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames( + KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames( + KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames( + KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames( + KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames( + KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames( + KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames( + KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM + ) + } @OptIn(ExperimentalContracts::class) internal inline fun <T> getState(action: GetStateScope.() -> T): T { diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 754f77ec38f93b18dcca20471a3a07f30d4852d0..29fe95c1e2520aac564fa148737e1d8687b5e37a 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -262,17 +262,8 @@ private constructor( forEachSchemePolicy { with(it) { onPackageUninstalled(packageName, appId, userId) } } } - fun MutateStateScope.onSystemReady( - packageStates: Map<String, PackageState>, - disabledSystemPackageStates: Map<String, PackageState>, - knownPackages: IntMap<Array<String>> - ) { - newState.mutateExternalState().apply { - setPackageStates(packageStates) - setDisabledSystemPackageStates(disabledSystemPackageStates) - setKnownPackages(knownPackages) - setSystemReady(true) - } + fun MutateStateScope.onSystemReady() { + newState.mutateExternalState().setSystemReady(true) forEachSchemePolicy { with(it) { onSystemReady() } } } diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 08ba75397a09292e60885cbb57aa50b826387414..010604f9aaaabc66b8595b71f167fe88342a6562 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -1448,15 +1448,6 @@ class AppIdPermissionPolicy : SchemePolicy() { // Special permissions for the system companion device manager. return true } - if ( - permission.isRetailDemo && - packageName in knownPackages[KnownPackages.PACKAGE_RETAIL_DEMO]!! - ) { - // Special permission granted only to the OEM specified retail demo app. - // Note that the original code was passing app ID as UID, so this behavior is kept - // unchanged. - return true - } if (permission.isRecents && packageName in knownPackages[KnownPackages.PACKAGE_RECENTS]!!) { // Special permission for the recents app. return true @@ -1511,27 +1502,6 @@ class AppIdPermissionPolicy : SchemePolicy() { } override fun MutateStateScope.onSystemReady() { - // HACK: PACKAGE_USAGE_STATS is the only permission with the retailDemo protection flag, - // and we have to wait until DevicePolicyManagerService is started to know whether the - // retail demo package is a profile owner so that it can have the permission. - // Since there's no simple callback for profile owner change, and we are deprecating and - // removing the retailDemo protection flag in favor of a proper role soon, we can just - // re-evaluate the permission here, which is also how the old implementation has been - // working. - // TODO: Partially revert ag/22690114 once we can remove support for the retailDemo - // protection flag. - val externalState = newState.externalState - for (packageName in externalState.knownPackages[KnownPackages.PACKAGE_RETAIL_DEMO]!!) { - val appId = externalState.packageStates[packageName]?.appId ?: continue - newState.userStates.forEachIndexed { _, userId, _ -> - evaluatePermissionState( - appId, - userId, - Manifest.permission.PACKAGE_USAGE_STATS, - null - ) - } - } if (!privilegedPermissionAllowlistViolations.isEmpty()) { throw IllegalStateException( "Signature|privileged permissions not in privileged" + diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 163d248fa317c1a9ba57c1d74254e40488c54a24..c7b1abf646f02e4d5af267aefd5037234208319c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -28,6 +28,8 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESE import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.server.display.DisplayManagerService.ENABLE_ON_CONNECT; import static com.android.server.display.VirtualDisplayAdapter.UNIQUE_ID_PREFIX; import static com.google.common.truth.Truth.assertThat; @@ -90,12 +92,14 @@ import android.hardware.display.VirtualDisplayConfig; import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionManager; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; import android.os.Process; import android.os.RemoteException; +import android.os.SystemProperties; import android.platform.test.flag.junit.SetFlagsRule; import android.view.ContentRecordingSession; import android.view.Display; @@ -113,6 +117,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; +import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; @@ -130,6 +135,7 @@ import com.google.common.truth.Expect; import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -141,6 +147,8 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.time.Duration; import java.util.ArrayList; @@ -322,6 +330,12 @@ public class DisplayManagerServiceTest { @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor; @Mock DisplayManagerFlags mMockFlags; + @Rule + public final ExtendedMockitoRule mExtendedMockitoRule = + new ExtendedMockitoRule.Builder(this) + .setStrictness(Strictness.LENIENT) + .spyStatic(SystemProperties.class) + .build(); @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -2405,6 +2419,39 @@ public class DisplayManagerServiceTest { EVENT_DISPLAY_CONNECTED).inOrder(); } + @Test + public void testConnectExternalDisplay_withDisplayManagementAndSysprop_shouldEnableDisplay() { + Assume.assumeTrue(Build.IS_ENG || Build.IS_USERDEBUG); + when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + doAnswer((Answer<Boolean>) invocationOnMock -> true) + .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); + manageDisplaysPermission(/* granted= */ true); + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService bs = displayManager.new BinderService(); + LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); + FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); + bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); + localService.registerDisplayGroupListener(callback); + callback.expectsEvent(EVENT_DISPLAY_ADDED); + + // Create default display device + createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL); + callback.waitForExpectedEvent(); + callback.clear(); + + callback.expectsEvent(EVENT_DISPLAY_CONNECTED); + FakeDisplayDevice displayDevice = + createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL); + callback.waitForExpectedEvent(); + + LogicalDisplay display = + logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ false); + assertThat(display.isEnabledLocked()).isTrue(); + assertThat(callback.receivedEvents()).containsExactly(DISPLAY_GROUP_EVENT_ADDED, + EVENT_DISPLAY_CONNECTED, EVENT_DISPLAY_ADDED).inOrder(); + } + @Test public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() { when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); diff --git a/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java b/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java index cc97b8f792f91838f02464242bccc5d78088ff80..76a1c3cae8dd20cc5cf243392946d1a5edfb5f3f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java @@ -154,10 +154,11 @@ public final class MasterClearReceiverTest { intent.putExtra(Intent.EXTRA_REASON, "Self destruct"); intent.putExtra(Intent.EXTRA_FORCE_FACTORY_RESET, true); intent.putExtra(Intent.EXTRA_WIPE_ESIMS, true); + intent.putExtra("keep_memtag_mode", true); mReceiver.onReceive(mContext, intent); verifyRebootWipeUserData(/* shutdown= */ true, /* reason= */ "Self destruct", - /* force= */ true, /* wipeEuicc= */ true); + /* force= */ true, /* wipeEuicc= */ true, /* keepMemtagMode= */ true); verifyWipeExternalData(); } @@ -211,7 +212,7 @@ public final class MasterClearReceiverTest { mRebootWipeUserDataLatch.countDown(); return null; }).when(() -> RecoverySystem - .rebootWipeUserData(any(), anyBoolean(), any(), anyBoolean(), anyBoolean())); + .rebootWipeUserData(any(), anyBoolean(), any(), anyBoolean(), anyBoolean(), anyBoolean())); } private void expectWipeExternalData() { @@ -244,11 +245,16 @@ public final class MasterClearReceiverTest { private void verifyRebootWipeUserData(boolean shutdown, String reason, boolean force, boolean wipeEuicc) throws Exception { + verifyRebootWipeUserData(shutdown, reason, force, wipeEuicc, /* keepMemtagMode= */ false); + } + + private void verifyRebootWipeUserData(boolean shutdown, String reason, boolean force, + boolean wipeEuicc, boolean keepMemtagMode) throws Exception { boolean called = mRebootWipeUserDataLatch.await(5, TimeUnit.SECONDS); assertWithMessage("rebootWipeUserData not called in 5s").that(called).isTrue(); verify(()-> RecoverySystem.rebootWipeUserData(same(mContext), eq(shutdown), eq(reason), - eq(force), eq(wipeEuicc))); + eq(force), eq(wipeEuicc), eq(keepMemtagMode))); } private void verifyNoRebootWipeUserData() { diff --git a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java index 4ffa0fbec75858f76005be0bce2da712e268070b..5f9a17c7ae98a8ab48d411b0cc6e079566abc105 100644 --- a/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/devicepolicy/FactoryResetterTest.java @@ -88,7 +88,7 @@ public final class FactoryResetterTest { Log.d(TAG, "Mocking " + inv); return null; }).when(() -> RecoverySystem.rebootWipeUserData(any(), anyBoolean(), any(), - anyBoolean(), anyBoolean())); + anyBoolean(), anyBoolean(), anyBoolean())); } @After @@ -270,17 +270,20 @@ public final class FactoryResetterTest { private void verifyRebootWipeUserDataMinimumArgsCalled() { verify(() -> RecoverySystem.rebootWipeUserData(mContext, /* shutdown= */ false, - /* reason= */ null, /* force= */ false, /* wipeEuicc= */ false)); + /* reason= */ null, /* force= */ false, /* wipeEuicc= */ false, + /* keepMemtagMode= */ false)); } private void verifyRebootWipeUserDataMinimumArgsButForceCalled() { verify(() -> RecoverySystem.rebootWipeUserData(mContext, /* shutdown= */ false, - /* reason= */ null, /* force= */ true, /* wipeEuicc= */ false)); + /* reason= */ null, /* force= */ true, /* wipeEuicc= */ false, + /* keepMemtagMode= */ false)); } private void verifyRebootWipeUserDataAllArgsCalled() { verify(() -> RecoverySystem.rebootWipeUserData(mContext, /* shutdown= */ true, - /* reason= */ REASON, /* force= */ true, /* wipeEuicc= */ true)); + /* reason= */ REASON, /* force= */ true, /* wipeEuicc= */ true, + /* keepMemtagMode= */ false)); } private void verifyWipeAdoptableStorageNotCalled() { 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 dd23d9f006e324f6c910eb1dd87335d4392ed9dd..e5291d31d5246401c33b575219761cd47a282421 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -577,8 +577,14 @@ public class JobSchedulerServiceTest { JobStatus jobUIDT = createJobStatus("testGetMaxJobExecutionTimeMs", createJobInfo(10) .setUserInitiated(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)); + JobStatus jobEj = createJobStatus("testGetMaxJobExecutionTimeMs", + createJobInfo(2).setExpedited(true)); + JobStatus jobReg = createJobStatus("testGetMaxJobExecutionTimeMs", + createJobInfo(3)); spyOn(jobUIDT); when(jobUIDT.shouldTreatAsUserInitiatedJob()).thenReturn(true); + spyOn(jobEj); + when(jobEj.shouldTreatAsExpeditedJob()).thenReturn(true); QuotaController quotaController = mService.getQuotaController(); spyOn(quotaController); @@ -595,6 +601,11 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUIDT)); + + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMaxJobExecutionTimeMs(jobEj)); + assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mService.getMaxJobExecutionTimeMs(jobReg)); } @Test @@ -636,7 +647,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -649,7 +660,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -664,7 +675,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -677,7 +688,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -692,7 +703,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -705,7 +716,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -720,7 +731,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -765,7 +776,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -778,7 +789,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -792,7 +803,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -807,7 +818,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -820,7 +831,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); @@ -834,7 +845,7 @@ public class JobSchedulerServiceTest { grantRunUserInitiatedJobsPermission(false); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobUij)); - assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMaxJobExecutionTimeMs(jobEj)); assertEquals(mService.mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, mService.getMaxJobExecutionTimeMs(jobReg)); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..09935f24cf93b630211a8d5932d4a2acf7157652 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/idle/DeviceIdlenessTrackerTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers.idle; + +import static android.text.format.DateUtils.HOUR_IN_MILLIS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +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.JobSchedulerService.sElapsedRealtimeClock; +import static com.android.server.job.controllers.idle.DeviceIdlenessTracker.KEY_INACTIVITY_IDLE_THRESHOLD_MS; +import static com.android.server.job.controllers.idle.DeviceIdlenessTracker.KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.AlarmManager; +import android.app.UiModeManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.PowerManager; +import android.os.SystemClock; +import android.provider.DeviceConfig; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.AppSchedulingModuleThread; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZoneOffset; + +@RunWith(AndroidJUnit4.class) +public class DeviceIdlenessTrackerTest { + private DeviceIdlenessTracker mDeviceIdlenessTracker; + private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); + private BroadcastReceiver mBroadcastReceiver; + private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder = + new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);; + + private MockitoSession mMockingSession; + @Mock + private AlarmManager mAlarmManager; + @Mock + private Context mContext; + @Mock + private JobSchedulerService mJobSchedulerService; + @Mock + private PowerManager mPowerManager; + @Mock + private Resources mResources; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(DeviceConfig.class) + .mockStatic(LocalServices.class) + .startMocking(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + when(mJobSchedulerService.getConstants()).thenReturn(mConstants); + // Called in DeviceIdlenessTracker.startTracking. + when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager); + when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); + when(mContext.getResources()).thenReturn(mResources); + doReturn((int) (31 * MINUTE_IN_MILLIS)).when(mResources).getInteger( + com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold); + doReturn((int) (17 * MINUTE_IN_MILLIS)).when(mResources).getInteger( + com.android.internal.R.integer + .config_jobSchedulerInactivityIdleThresholdOnStablePower); + doReturn(mPowerManager).when(() -> LocalServices.getService(PowerManager.class)); + + // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions + // in the past, and QuotaController sometimes floors values at 0, so if the test time + // causes sessions with negative timestamps, they will fail. + JobSchedulerService.sSystemClock = + getAdvancedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sUptimeMillisClock = getAdvancedClock( + Clock.fixed(SystemClock.uptimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC), + 24 * HOUR_IN_MILLIS); + + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + mDeviceIdlenessTracker = new DeviceIdlenessTracker(); + mDeviceIdlenessTracker.startTracking(mContext, + mJobSchedulerService, mock(IdlenessListener.class)); + + verify(mContext).registerReceiver(broadcastReceiverCaptor.capture(), any(), any(), any()); + mBroadcastReceiver = broadcastReceiverCaptor.getValue(); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private Clock getAdvancedClock(Clock clock, long incrementMs) { + return Clock.offset(clock, Duration.ofMillis(incrementMs)); + } + + private void advanceElapsedClock(long incrementMs) { + JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock( + JobSchedulerService.sElapsedRealtimeClock, incrementMs); + } + + private void setBatteryState(boolean isCharging, boolean isBatteryNotLow) { + doReturn(isCharging).when(mJobSchedulerService).isBatteryCharging(); + doReturn(isBatteryNotLow).when(mJobSchedulerService).isBatteryNotLow(); + mDeviceIdlenessTracker.onBatteryStateChanged(isCharging, isBatteryNotLow); + } + + private void setDeviceConfigLong(String key, long val) { + mDeviceConfigPropertiesBuilder.setLong(key, val); + mDeviceIdlenessTracker.processConstant(mDeviceConfigPropertiesBuilder.build(), key); + } + + @Test + public void testThresholdChangeWithStablePowerChange() { + setDeviceConfigLong(KEY_INACTIVITY_IDLE_THRESHOLD_MS, 10 * MINUTE_IN_MILLIS); + setDeviceConfigLong(KEY_INACTIVITY_STABLE_POWER_IDLE_THRESHOLD_MS, 5 * MINUTE_IN_MILLIS); + setBatteryState(false, false); + + Intent screenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF); + mBroadcastReceiver.onReceive(mContext, screenOffIntent); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + long expectedUnstableAlarmElapsed = nowElapsed + 10 * MINUTE_IN_MILLIS; + long expectedStableAlarmElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS; + + InOrder inOrder = inOrder(mAlarmManager); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedUnstableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Advanced the clock a little to make sure the tracker continues to use the original time. + advanceElapsedClock(MINUTE_IN_MILLIS); + + // Charging isn't enough for stable power. + setBatteryState(true, false); + inOrder.verify(mAlarmManager, never()) + .setWindow(anyInt(), anyLong(), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Now on stable power. + setBatteryState(true, true); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedStableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Battery-not-low isn't enough for stable power. Go back to unstable timing. + setBatteryState(false, true); + inOrder.verify(mAlarmManager) + .setWindow(anyInt(), eq(expectedUnstableAlarmElapsed), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + + // Still not on stable power. + setBatteryState(false, false); + inOrder.verify(mAlarmManager, never()) + .setWindow(anyInt(), anyLong(), anyLong(), anyString(), + eq(AppSchedulingModuleThread.getExecutor()), any()); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt index eb001645863f30c3a6d2dbc330494adb8a57ce00..a6ba5d4c30328b4ecf20348a31a50c24da9af00d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt @@ -87,8 +87,8 @@ open class PackageHelperTestBase { TEST_PACKAGE_1, TEST_PACKAGE_2, DEVICE_OWNER_PACKAGE, DEVICE_ADMIN_PACKAGE, DEFAULT_HOME_PACKAGE, DIALER_PACKAGE, INSTALLER_PACKAGE, UNINSTALLER_PACKAGE, VERIFIER_PACKAGE, PERMISSION_CONTROLLER_PACKAGE)) - suspendPackageHelper = SuspendPackageHelper(pms, rule.mocks().injector, - rule.mocks().userManagerService, broadcastHelper, protectedPackages) + suspendPackageHelper = SuspendPackageHelper( + pms, rule.mocks().injector, broadcastHelper, protectedPackages) defaultAppProvider = rule.mocks().defaultAppProvider testHandler = rule.mocks().handler packageSetting1 = pms.snapshotComputer().getPackageStateInternal(TEST_PACKAGE_1)!! diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt index 4240373b7c1dda7c6f97a0a68138bf2dd546e7d1..7b381ce443e18a77e387756c88d1fe1f9b6d5ddf 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt @@ -39,7 +39,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), targetPackages, true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) testHandler.flush() verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID)) @@ -56,14 +56,14 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), null /* packageNames */, true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) assertThat(failedNames).isNull() failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOfNulls(0), true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) assertThat(failedNames).isEmpty() } @@ -73,7 +73,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, TEST_PACKAGE_1, TEST_USER_ID, - Binder.getCallingUid(), false /* forQuietMode */, false /* quarantined */) + Binder.getCallingUid(), false /* quarantined */) assertThat(failedNames).asList().hasSize(1) assertThat(failedNames).asList().contains(TEST_PACKAGE_2) @@ -84,7 +84,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(DEVICE_OWNER_PACKAGE), true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) assertThat(failedNames).asList().hasSize(1) assertThat(failedNames).asList().contains(DEVICE_OWNER_PACKAGE) @@ -95,7 +95,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(NONEXISTENT_PACKAGE), true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) assertThat(failedNames).asList().hasSize(1) assertThat(failedNames).asList().contains(NONEXISTENT_PACKAGE) @@ -108,7 +108,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), knownPackages, true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */)!! + TEST_USER_ID, deviceOwnerUid, false /* quarantined */)!! assertThat(failedNames.size).isEqualTo(knownPackages.size) for (pkg in knownPackages) { @@ -116,34 +116,20 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { } } - @Test - fun setPackagesSuspended_forQuietMode() { - val knownPackages = arrayOf(DEVICE_ADMIN_PACKAGE, DEFAULT_HOME_PACKAGE, DIALER_PACKAGE, - INSTALLER_PACKAGE, UNINSTALLER_PACKAGE, VERIFIER_PACKAGE, - PERMISSION_CONTROLLER_PACKAGE, MGMT_ROLE_HOLDER_PACKAGE) - val failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), - knownPackages, true /* suspended */, null /* appExtras */, - null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, true /* forQuietMode */, false /* quarantined */)!! - - assertThat(failedNames.size).isEqualTo(1) - assertThat(failedNames[0]).isEqualTo(MGMT_ROLE_HOLDER_PACKAGE) - } - @Test fun setPackagesUnsuspended() { val targetPackages = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2) var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), targetPackages, true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) testHandler.flush() Mockito.clearInvocations(broadcastHelper) assertThat(failedNames).isEmpty() failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), targetPackages, false /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) testHandler.flush() verify(pms, times(2)).scheduleWritePackageRestrictions(eq(TEST_USER_ID)) @@ -191,7 +177,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_1), true /* suspended */, appExtras, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, - false /* forQuietMode */, false /* quarantined */) + false /* quarantined */) testHandler.flush() assertThat(failedNames).isEmpty() @@ -209,7 +195,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), targetPackages, true /* suspended */, appExtras, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, - false /* forQuietMode */, false /* quarantined */) + false /* quarantined */) testHandler.flush() Mockito.clearInvocations(broadcastHelper) assertThat(failedNames).isEmpty() @@ -250,7 +236,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */, launcherExtras, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, - false /* forQuietMode */, false /* quarantined */) + false /* quarantined */) testHandler.flush() assertThat(failedNames).isEmpty() @@ -265,7 +251,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_1), true /* suspended */, null /* appExtras */, null /* launcherExtras */, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, - TEST_USER_ID, deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + TEST_USER_ID, deviceOwnerUid, false /* quarantined */) testHandler.flush() assertThat(failedNames).isEmpty() @@ -280,7 +266,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_2), true /* suspended */, null /* appExtras */, launcherExtras, null /* dialogInfo */, DEVICE_OWNER_PACKAGE, TEST_USER_ID, deviceOwnerUid, - false /* forQuietMode */, false /* quarantined */) + false /* quarantined */) testHandler.flush() assertThat(failedNames).isEmpty() @@ -295,7 +281,7 @@ class SuspendPackageHelperTest : PackageHelperTestBase() { var failedNames = suspendPackageHelper.setPackagesSuspended(pms.snapshotComputer(), arrayOf(TEST_PACKAGE_1), true /* suspended */, null /* appExtras */, null /* launcherExtras */, dialogInfo, DEVICE_OWNER_PACKAGE, TEST_USER_ID, - deviceOwnerUid, false /* forQuietMode */, false /* quarantined */) + deviceOwnerUid, false /* quarantined */) testHandler.flush() assertThat(failedNames).isEmpty() diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index f88afe7839c31afe650b67ecb43dc886225484f1..a78f2dcf2ab272c8c42c7cea2791bd394520f8f2 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -41,6 +41,8 @@ import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricService; import android.hardware.biometrics.IBiometricServiceReceiver; @@ -53,6 +55,7 @@ import android.hardware.iris.IIrisService; import android.os.Binder; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -84,6 +87,8 @@ public class AuthServiceTest { @Rule public MockitoRule mockitorule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private Context mContext; @Mock @@ -418,6 +423,37 @@ public class AuthServiceTest { eq(callback)); } + @Test(expected = UnsupportedOperationException.class) + public void testGetLastAuthenticationTime_flaggedOff_throwsUnsupportedOperationException() + throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); + setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); + + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + mAuthService.mImpl.getLastAuthenticationTime(0, + BiometricManager.Authenticators.BIOMETRIC_STRONG); + } + + @Test + public void testGetLastAuthenticationTime_flaggedOn_callsBiometricService() + throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); + setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); + + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final int userId = 0; + final int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG; + + mAuthService.mImpl.getLastAuthenticationTime(userId, authenticators); + + waitForIdle(); + verify(mBiometricService).getLastAuthenticationTime(eq(userId), eq(authenticators)); + } + private static void setInternalAndTestBiometricPermissions( Context context, boolean hasPermission) { for (String p : List.of(TEST_BIOMETRIC, MANAGE_BIOMETRIC, USE_BIOMETRIC_INTERNAL)) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 0230d77e8e14793bf3fb35ea10572b7edf59c6e3..408442bcceedfdfb68ff5eacfdc847d4ce0e021e 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -62,6 +62,7 @@ import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; +import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; import android.hardware.biometrics.IBiometricSensorReceiver; @@ -71,12 +72,17 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManagerGlobal; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.keymaster.HardwareAuthenticatorType; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.security.GateKeeper; import android.security.KeyStore; +import android.security.authorization.IKeystoreAuthorization; +import android.service.gatekeeper.IGateKeeperService; import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; @@ -92,6 +98,7 @@ import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.LockoutTracker; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.AdditionalMatchers; import org.mockito.ArgumentCaptor; @@ -105,6 +112,9 @@ import java.util.Random; @SmallTest public class BiometricServiceTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String TEST_PACKAGE_NAME = "test_package"; private static final long TEST_REQUEST_ID = 44; @@ -162,10 +172,16 @@ public class BiometricServiceTest { @Mock private BiometricCameraManager mBiometricCameraManager; + @Mock + private IKeystoreAuthorization mKeystoreAuthService; + + @Mock + private IGateKeeperService mGateKeeperService; + BiometricContextProvider mBiometricContextProvider; @Before - public void setUp() { + public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); resetReceivers(); @@ -215,6 +231,9 @@ public class BiometricServiceTest { mStatusBarService, null /* handler */, mAuthSessionCoordinator); when(mInjector.getBiometricContext(any())).thenReturn(mBiometricContextProvider); + when(mInjector.getKeystoreAuthorizationService()).thenReturn(mKeystoreAuthService); + when(mInjector.getGateKeeperService()).thenReturn(mGateKeeperService); + when(mGateKeeperService.getSecureUserId(anyInt())).thenReturn(42L); final String[] config = { "0:2:15", // ID0:Fingerprint:Strong @@ -1751,6 +1770,44 @@ public class BiometricServiceTest { verifyNoMoreInteractions(callback); } + @Test(expected = UnsupportedOperationException.class) + public void testGetLastAuthenticationTime_flagOff_throwsUnsupportedOperationException() + throws RemoteException { + mSetFlagsRule.disableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); + + mBiometricService = new BiometricService(mContext, mInjector); + mBiometricService.mImpl.getLastAuthenticationTime(0, Authenticators.BIOMETRIC_STRONG); + } + + @Test + public void testGetLastAuthenticationTime_flagOn_callsKeystoreAuthorization() + throws RemoteException { + mSetFlagsRule.enableFlags(Flags.FLAG_LAST_AUTHENTICATION_TIME); + + final int[] hardwareAuthenticators = new int[] { + HardwareAuthenticatorType.PASSWORD, + HardwareAuthenticatorType.FINGERPRINT + }; + + final int userId = 0; + final long secureUserId = mGateKeeperService.getSecureUserId(userId); + + assertNotEquals(GateKeeper.INVALID_SECURE_USER_ID, secureUserId); + + final long expectedResult = 31337L; + + when(mKeystoreAuthService.getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators))) + .thenReturn(expectedResult); + + mBiometricService = new BiometricService(mContext, mInjector); + + final long result = mBiometricService.mImpl.getLastAuthenticationTime(userId, + Authenticators.BIOMETRIC_STRONG | Authenticators.DEVICE_CREDENTIAL); + + assertEquals(expectedResult, result); + verify(mKeystoreAuthService).getLastAuthTime(eq(secureUserId), eq(hardwareAuthenticators)); + } + // Helper methods private int invokeCanAuthenticate(BiometricService service, int authenticators) 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 461d63745c3164ff74ff290ee9b215a90a38046a..2598a6bea1c0b28ccdb8357f678ae44874aa3633 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 @@ -347,6 +347,8 @@ public class VirtualDeviceManagerServiceTest { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.uniqueId = UNIQUE_ID; doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt()); + doReturn(Display.INVALID_DISPLAY).when(mDisplayManagerInternalMock) + .getDisplayIdToMirror(anyInt()); LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); @@ -1627,6 +1629,7 @@ public class VirtualDeviceManagerServiceTest { @Test public void openNonBlockedAppOnMirrorDisplay_flagDisabled_launchesActivity() { + mSetFlagsRule.disableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR); when(mDisplayManagerInternalMock.getDisplayIdToMirror(anyInt())) .thenReturn(Display.DEFAULT_DISPLAY); addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java index af633cc75b4db0ca57f7c627f6c1978e98c6e0be..dbd6c889622ad5e8ba72b17947db52c5e2eac5f3 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java @@ -37,6 +37,7 @@ import android.net.MacAddress; import android.os.Binder; import android.testing.TestableContext; import android.util.ArraySet; +import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; @@ -137,6 +138,8 @@ public class VirtualDeviceRule implements TestRule { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.uniqueId = "uniqueId"; doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt()); + doReturn(Display.INVALID_DISPLAY).when(mDisplayManagerInternalMock) + .getDisplayIdToMirror(anyInt()); LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java index 568471d67c315fe37b8241d4056b4c3a7152af77..526201f9c1c6528084698591d3db5f55c5677151 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java @@ -247,39 +247,9 @@ public class ActivityStartInterceptorTest { } @Test - public void testInterceptQuietProfile_keepProfilesRunningEnabled() { - // GIVEN that the user the activity is starting as is currently in quiet mode and - // profiles are kept running when in quiet mode. + public void testInterceptQuietProfile() { + // GIVEN that the user the activity is starting as is currently in quiet mode when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); - when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); - - // THEN calling intercept returns false because package also has to be suspended. - assertFalse( - mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); - } - - @Test - public void testInterceptQuietProfile_keepProfilesRunningDisabled() { - // GIVEN that the user the activity is starting as is currently in quiet mode and - // profiles are stopped when in quiet mode (pre-U behavior, no profile app suspension). - when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); - when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); - - // THEN calling intercept returns true - assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); - - // THEN the returned intent is the quiet mode intent - assertTrue(UnlaunchableAppActivity.createInQuietModeDialogIntent(TEST_USER_ID) - .filterEquals(mInterceptor.mIntent)); - } - - @Test - public void testInterceptQuietProfileWhenPackageSuspended_keepProfilesRunningEnabled() { - // GIVEN that the user the activity is starting as is currently in quiet mode, - // the package is suspended and profiles are kept running while in quiet mode. - suspendPackage("com.test.suspending.package"); - when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); - when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(true); // THEN calling intercept returns true assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); @@ -290,12 +260,10 @@ public class ActivityStartInterceptorTest { } @Test - public void testInterceptQuietProfileWhenPackageSuspended_keepProfilesRunningDisabled() { - // GIVEN that the user the activity is starting as is currently in quiet mode, - // the package is suspended and profiles are stopped while in quiet mode. + public void testInterceptQuietProfileWhenPackageSuspended() { suspendPackage("com.test.suspending.package"); + // GIVEN that the user the activity is starting as is currently in quiet mode when(mUserManager.isQuietModeEnabled(eq(UserHandle.of(TEST_USER_ID)))).thenReturn(true); - when(mDevicePolicyManager.isKeepProfilesRunningEnabled()).thenReturn(false); // THEN calling intercept returns true assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, null, 0, 0, null, null)); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 4c25a4bf1455cc90e50f814783272d5aadcc77ba..3b4b220907f79d05f7a5e098825b21ea410d18af 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -1444,7 +1444,7 @@ public class RecentTasksTest extends WindowTestsBase { }); assertSecurityException(expectCallable, () -> mAtm.startActivityFromRecents(0, new Bundle())); - assertSecurityException(expectCallable, () -> mAtm.getTaskSnapshot(0, true, false)); + assertSecurityException(expectCallable, () -> mAtm.getTaskSnapshot(0, true)); assertSecurityException(expectCallable, () -> mAtm.registerTaskStackListener(null)); assertSecurityException(expectCallable, () -> mAtm.unregisterTaskStackListener(null)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 435a8357dabb481a833661614bf58cb1d236308c..0639deb2fdcab1af4c79767990bc9a826cb0cbb8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -73,6 +73,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; @@ -570,12 +571,15 @@ public class TaskTests extends WindowTestsBase { .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build(); final Task task = rootTask.getBottomMostTask(); final ActivityRecord root = task.getTopNonFinishingActivity(); + final PackageManager pm = mContext.getPackageManager(); + spyOn(pm); spyOn(mWm.mLetterboxConfiguration); spyOn(root); spyOn(root.mLetterboxUiController); doReturn(true).when(root.mLetterboxUiController) .shouldEnableUserAspectRatioSettings(); + doReturn(new Intent()).when(pm).getLaunchIntentForPackage(anyString()); doReturn(false).when(root).inSizeCompatMode(); doReturn(task).when(root).getOrganizedTask(); @@ -593,6 +597,10 @@ public class TaskTests extends WindowTestsBase { doReturn(true).when(root).inSizeCompatMode(); assertFalse(task.getTaskInfo().topActivityEligibleForUserAspectRatioButton); doReturn(false).when(root).inSizeCompatMode(); + + // When app doesn't have any launchable activities the button is not enabled + doReturn(null).when(pm).getLaunchIntentForPackage(anyString()); + assertFalse(task.getTaskInfo().topActivityEligibleForUserAspectRatioButton); } /** diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index e413663160b59eaf631478a00735ad93e74e82a8..f64ab22628d97a252d9ac0a6752fa947acee7725 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -57,6 +57,7 @@ import android.app.usage.Flags; import android.app.usage.IUsageStatsManager; import android.app.usage.UsageEvents; import android.app.usage.UsageEvents.Event; +import android.app.usage.UsageEventsQuery; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManager.StandbyBuckets; @@ -113,6 +114,8 @@ import com.android.server.SystemService; import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener; import com.android.server.utils.AlarmQueue; +import libcore.util.EmptyArray; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -1478,6 +1481,14 @@ public class UsageStatsService extends SystemService implements * Called by the Binder stub. */ UsageEvents queryEvents(int userId, long beginTime, long endTime, int flags) { + return queryEventsWithTypes(userId, beginTime, endTime, flags, EmptyArray.INT); + } + + /** + * Called by the Binder stub. + */ + UsageEvents queryEventsWithTypes(int userId, long beginTime, long endTime, int flags, + int[] eventTypeFilter) { synchronized (mLock) { if (!mUserUnlockedStates.contains(userId)) { Slog.w(TAG, "Failed to query events for locked user " + userId); @@ -1488,7 +1499,7 @@ public class UsageStatsService extends SystemService implements if (service == null) { return null; // user was stopped or removed } - return service.queryEvents(beginTime, endTime, flags); + return service.queryEvents(beginTime, endTime, flags, eventTypeFilter); } } @@ -2123,7 +2134,7 @@ public class UsageStatsService extends SystemService implements private final class BinderService extends IUsageStatsManager.Stub { - private boolean hasPermission(String callingPackage) { + private boolean hasQueryPermission(String callingPackage) { final int callingUid = Binder.getCallingUid(); if (callingUid == Process.SYSTEM_UID) { return true; @@ -2203,10 +2214,37 @@ public class UsageStatsService extends SystemService implements return uid == Process.SYSTEM_UID; } + private UsageEvents queryEventsHelper(int userId, long beginTime, long endTime, + String callingPackage, int[] eventTypeFilter) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller( + callingUid, userId); + + final long token = Binder.clearCallingIdentity(); + try { + final boolean hideShortcutInvocationEvents = shouldHideShortcutInvocationEvents( + userId, callingPackage, callingPid, callingUid); + final boolean hideLocusIdEvents = shouldHideLocusIdEvents(callingPid, callingUid); + final boolean obfuscateNotificationEvents = shouldObfuscateNotificationEvents( + callingPid, callingUid); + int flags = UsageEvents.SHOW_ALL_EVENT_DATA; + if (obfuscateInstantApps) flags |= UsageEvents.OBFUSCATE_INSTANT_APPS; + if (hideShortcutInvocationEvents) flags |= UsageEvents.HIDE_SHORTCUT_EVENTS; + if (hideLocusIdEvents) flags |= UsageEvents.HIDE_LOCUS_EVENTS; + if (obfuscateNotificationEvents) flags |= UsageEvents.OBFUSCATE_NOTIFICATION_EVENTS; + + return UsageStatsService.this.queryEventsWithTypes(userId, beginTime, endTime, + flags, eventTypeFilter); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public ParceledListSlice<UsageStats> queryUsageStats(int bucketType, long beginTime, long endTime, String callingPackage, int userId) { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } @@ -2234,7 +2272,7 @@ public class UsageStatsService extends SystemService implements @Override public ParceledListSlice<ConfigurationStats> queryConfigurationStats(int bucketType, long beginTime, long endTime, String callingPackage) throws RemoteException { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } @@ -2256,7 +2294,7 @@ public class UsageStatsService extends SystemService implements @Override public ParceledListSlice<EventStats> queryEventStats(int bucketType, long beginTime, long endTime, String callingPackage) throws RemoteException { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } @@ -2277,32 +2315,25 @@ public class UsageStatsService extends SystemService implements @Override public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } - final int userId = UserHandle.getCallingUserId(); - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller( - callingUid, userId); + return queryEventsHelper(UserHandle.getCallingUserId(), beginTime, endTime, + callingPackage, /* eventTypeFilter= */ EmptyArray.INT); + } - final long token = Binder.clearCallingIdentity(); - try { - final boolean hideShortcutInvocationEvents = shouldHideShortcutInvocationEvents( - userId, callingPackage, callingPid, callingUid); - final boolean hideLocusIdEvents = shouldHideLocusIdEvents(callingPid, callingUid); - final boolean obfuscateNotificationEvents = shouldObfuscateNotificationEvents( - callingPid, callingUid); - int flags = UsageEvents.SHOW_ALL_EVENT_DATA; - if (obfuscateInstantApps) flags |= UsageEvents.OBFUSCATE_INSTANT_APPS; - if (hideShortcutInvocationEvents) flags |= UsageEvents.HIDE_SHORTCUT_EVENTS; - if (hideLocusIdEvents) flags |= UsageEvents.HIDE_LOCUS_EVENTS; - if (obfuscateNotificationEvents) flags |= UsageEvents.OBFUSCATE_NOTIFICATION_EVENTS; - return UsageStatsService.this.queryEvents(userId, beginTime, endTime, flags); - } finally { - Binder.restoreCallingIdentity(token); + @Override + public UsageEvents queryEventsWithFilter(@NonNull UsageEventsQuery query, + @NonNull String callingPackage) { + Objects.requireNonNull(query); + Objects.requireNonNull(callingPackage); + + if (!hasQueryPermission(callingPackage)) { + return null; } + return queryEventsHelper(UserHandle.getCallingUserId(), query.getBeginTimeMillis(), + query.getEndTimeMillis(), callingPackage, query.getEventTypeFilter()); } @Override @@ -2312,7 +2343,7 @@ public class UsageStatsService extends SystemService implements final int callingUserId = UserHandle.getUserId(callingUid); checkCallerIsSameApp(callingPackage); - final boolean includeTaskRoot = hasPermission(callingPackage); + final boolean includeTaskRoot = hasQueryPermission(callingPackage); final long token = Binder.clearCallingIdentity(); try { @@ -2326,7 +2357,7 @@ public class UsageStatsService extends SystemService implements @Override public UsageEvents queryEventsForUser(long beginTime, long endTime, int userId, String callingPackage) { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } @@ -2337,33 +2368,14 @@ public class UsageStatsService extends SystemService implements "No permission to query usage stats for this user"); } - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller( - callingUid, callingUserId); - - final long token = Binder.clearCallingIdentity(); - try { - final boolean hideShortcutInvocationEvents = shouldHideShortcutInvocationEvents( - userId, callingPackage, callingPid, callingUid); - final boolean obfuscateNotificationEvents = shouldObfuscateNotificationEvents( - callingPid, callingUid); - boolean hideLocusIdEvents = shouldHideLocusIdEvents(callingPid, callingUid); - int flags = UsageEvents.SHOW_ALL_EVENT_DATA; - if (obfuscateInstantApps) flags |= UsageEvents.OBFUSCATE_INSTANT_APPS; - if (hideShortcutInvocationEvents) flags |= UsageEvents.HIDE_SHORTCUT_EVENTS; - if (hideLocusIdEvents) flags |= UsageEvents.HIDE_LOCUS_EVENTS; - if (obfuscateNotificationEvents) flags |= UsageEvents.OBFUSCATE_NOTIFICATION_EVENTS; - return UsageStatsService.this.queryEvents(userId, beginTime, endTime, flags); - } finally { - Binder.restoreCallingIdentity(token); - } + return queryEventsHelper(userId, beginTime, endTime, callingPackage, + /* eventTypeFilter= */ EmptyArray.INT); } @Override public UsageEvents queryEventsForPackageForUser(long beginTime, long endTime, int userId, String pkg, String callingPackage) { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { return null; } if (userId != UserHandle.getCallingUserId()) { @@ -2404,7 +2416,7 @@ public class UsageStatsService extends SystemService implements if (actualCallingUid != callingUid) { return false; } - } else if (!hasPermission(callingPackage)) { + } else if (!hasQueryPermission(callingPackage)) { return false; } final boolean obfuscateInstantApps = shouldObfuscateInstantAppsForCaller( @@ -2454,7 +2466,7 @@ public class UsageStatsService extends SystemService implements final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); // If the calling app is asking about itself, continue, else check for permission. final boolean sameApp = packageUid == callingUid; - if (!sameApp && !hasPermission(callingPackage)) { + if (!sameApp && !hasQueryPermission(callingPackage)) { throw new SecurityException("Don't have permission to query app standby bucket"); } @@ -2502,7 +2514,7 @@ public class UsageStatsService extends SystemService implements } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } - if (!hasPermission(callingPackageName)) { + if (!hasQueryPermission(callingPackageName)) { throw new SecurityException( "Don't have permission to query app standby bucket"); } @@ -2556,7 +2568,7 @@ public class UsageStatsService extends SystemService implements final int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); // If the calling app is asking about itself, continue, else check for permission. if (packageUid != callingUid) { - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { throw new SecurityException( "Don't have permission to query min app standby bucket"); } @@ -2900,7 +2912,7 @@ public class UsageStatsService extends SystemService implements if (!hasPermissions(android.Manifest.permission.INTERACT_ACROSS_USERS)) { throw new SecurityException("Caller doesn't have INTERACT_ACROSS_USERS permission"); } - if (!hasPermission(callingPackage)) { + if (!hasQueryPermission(callingPackage)) { throw new SecurityException("Don't have permission to query usage stats"); } synchronized (mLock) { diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index ddb27969dd0ad24f69d266856a77886f7b8a53d4..9b67ab6d99c1ce0d66069bca847cd91bed51b7b2 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -70,7 +70,7 @@ import java.util.Set; * in UsageStatsService. */ class UserUsageStatsService { - private static final String TAG = "UsageStatsService"; + private static final String TAG = UsageStatsService.TAG; private static final boolean DEBUG = UsageStatsService.DEBUG; private static final SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final int sDateFormatFlags = @@ -535,10 +535,23 @@ class UserUsageStatsService { return queryStats(bucketType, beginTime, endTime, sEventStatsCombiner, true); } - UsageEvents queryEvents(final long beginTime, final long endTime, int flags) { + UsageEvents queryEvents(final long beginTime, final long endTime, int flags, + int[] eventTypeFilter) { if (!validRange(checkAndGetTimeLocked(), beginTime, endTime)) { return null; } + + // Ensure valid event type filter. + final boolean isQueryForAllEvents = ArrayUtils.isEmpty(eventTypeFilter); + final boolean[] queryEventFilter = new boolean[Event.MAX_EVENT_TYPE + 1]; + if (!isQueryForAllEvents) { + for (int eventType : eventTypeFilter) { + if (eventType < Event.NONE || eventType > Event.MAX_EVENT_TYPE) { + throw new IllegalArgumentException("invalid event type: " + eventType); + } + queryEventFilter[eventType] = true; + } + } final ArraySet<String> names = new ArraySet<>(); List<Event> results = queryStats(INTERVAL_DAILY, beginTime, endTime, new StatCombiner<Event>() { @@ -547,6 +560,7 @@ class UserUsageStatsService { List<Event> accumulatedResult) { final int startIndex = stats.events.firstIndexOnOrAfter(beginTime); final int size = stats.events.size(); + for (int i = startIndex; i < size; i++) { Event event = stats.events.get(i); if (event.mTimeStamp >= endTime) { @@ -554,6 +568,10 @@ class UserUsageStatsService { } final int eventType = event.mEventType; + if (!isQueryForAllEvents && !queryEventFilter[eventType]) { + continue; + } + if (eventType == Event.SHORTCUT_INVOCATION && (flags & HIDE_SHORTCUT_EVENTS) == HIDE_SHORTCUT_EVENTS) { continue; diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AndroidTestTemplate.xml index 85709c943a18d03a202a7583fd0af05b32d14427..ed71531d4eab4947b25adba8e0fdf934556cbbbf 100644 --- a/tests/FlickerTests/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AndroidTestTemplate.xml @@ -27,8 +27,14 @@ <option name="run-command" value="cmd window tracing size 20480"/> <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> <!-- b/307664397 - Ensure camera has the correct permissions and doesn't show a dialog --> + <option name="run-command" + value="pm grant com.google.android.GoogleCamera android.permission.CAMERA"/> + <option name="run-command" + value="pm grant com.google.android.GoogleCamera android.permission.RECORD_AUDIO"/> <option name="run-command" value="pm grant com.google.android.GoogleCamera android.permission.ACCESS_FINE_LOCATION"/> + <option name="run-command" + value="pm grant com.google.android.GoogleCamera android.permission.ACCESS_COARSE_LOCATION"/> </target_preparer> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java index cb3782173dc86bd626c29f1eb45f916151f0f4f5..59dc68900100c05bad40120ed4825b4adeda8adf 100644 --- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java @@ -313,7 +313,8 @@ public class VcnGatewayConnectionConfigTest { @Test public void testBuilderAndGettersSafeModeDisabled() { - final VcnGatewayConnectionConfig config = newBuilderMinimal().enableSafeMode(false).build(); + final VcnGatewayConnectionConfig config = + newBuilderMinimal().setSafeModeEnabled(false).build(); assertFalse(config.isSafeModeEnabled()); } @@ -335,7 +336,8 @@ public class VcnGatewayConnectionConfigTest { @Test public void testPersistableBundleSafeModeDisabled() { - final VcnGatewayConnectionConfig config = newBuilderMinimal().enableSafeMode(false).build(); + final VcnGatewayConnectionConfig config = + newBuilderMinimal().setSafeModeEnabled(false).build(); assertEquals(config, new VcnGatewayConnectionConfig(config.toPersistableBundle())); } @@ -456,7 +458,7 @@ public class VcnGatewayConnectionConfigTest { assertEquals(config.isSafeModeEnabled(), configEqual.isSafeModeEnabled()); final VcnGatewayConnectionConfig configNotEqual = - newBuilderMinimal().enableSafeMode(false).build(); + newBuilderMinimal().setSafeModeEnabled(false).build(); assertEquals(config, configEqual); assertNotEquals(config, configNotEqual); diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index bf73198d10068b7b7c2aeb884edc28a7c87d3d50..f8461642638912ababd1e7cf1872e5b8d79b9e33 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -661,7 +661,7 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection throws Exception { final VcnGatewayConnectionConfig config = VcnGatewayConnectionConfigTest.newTestBuilderMinimal() - .enableSafeMode(safeModeEnabledByCaller) + .setSafeModeEnabled(safeModeEnabledByCaller) .build(); final VcnGatewayConnection.Dependencies deps = mock(VcnGatewayConnection.Dependencies.class);