diff options
158 files changed, 4103 insertions, 1395 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 855366abae4e..81c3e8957cd1 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -87,10 +87,12 @@ import android.os.Parcelable; import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.StrictMode; import android.os.Trace; import android.os.UserHandle; +import android.service.voice.VoiceInteractionSession; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -154,6 +156,7 @@ import android.window.WindowOnBackInvokedDispatcher; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.ToolbarActionBar; import com.android.internal.app.WindowDecorActionBar; @@ -1601,6 +1604,25 @@ public class Activity extends ContextThemeWrapper return callbacks; } + private void notifyVoiceInteractionManagerServiceActivityEvent( + @VoiceInteractionSession.VoiceInteractionActivityEventType int type) { + + final IVoiceInteractionManagerService service = + IVoiceInteractionManagerService.Stub.asInterface( + ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); + if (service == null) { + Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get " + + "VoiceInteractionManagerService"); + return; + } + + try { + service.notifyActivityEventChanged(mToken, type); + } catch (RemoteException e) { + // Empty + } + } + /** * Called when the activity is starting. This is where most initialization * should go: calling {@link #setContentView(int)} to inflate the @@ -1876,6 +1898,9 @@ public class Activity extends ContextThemeWrapper mCalled = true; notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_START); + + notifyVoiceInteractionManagerServiceActivityEvent( + VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_START); } /** @@ -2019,6 +2044,12 @@ public class Activity extends ContextThemeWrapper final Window win = getWindow(); if (win != null) win.makeActive(); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true); + + // Because the test case "com.android.launcher3.jank.BinderTests#testPressHome" doesn't + // allow any binder call in onResume, we call this method in onPostResume. + notifyVoiceInteractionManagerServiceActivityEvent( + VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_RESUME); + mCalled = true; } @@ -2394,6 +2425,10 @@ public class Activity extends ContextThemeWrapper getAutofillClientController().onActivityPaused(); notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_PAUSE); + + notifyVoiceInteractionManagerServiceActivityEvent( + VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE); + mCalled = true; } @@ -2623,6 +2658,9 @@ public class Activity extends ContextThemeWrapper getAutofillClientController().onActivityStopped(mIntent, mChangingConfigurations); mEnterAnimationComplete = false; + + notifyVoiceInteractionManagerServiceActivityEvent( + VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP); } /** diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index f2ea060b1694..c95a7deba61f 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -731,10 +731,9 @@ public abstract class ActivityManagerInternal { */ public interface VoiceInteractionManagerProvider { /** - * Notifies the service when a high-level activity event has been changed, for example, - * an activity was resumed or stopped. + * Notifies the service when an activity is destroyed. */ - void notifyActivityEventChanged(); + void notifyActivityDestroyed(IBinder activityToken); } /** diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index a432b8dec2cb..fd9496947628 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -42,12 +42,15 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.util.DisplayMetrics; +import android.util.Log; import android.widget.RemoteViews; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.os.BackgroundThread; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * Updates AppWidget state; gets information about installed AppWidget providers and other @@ -63,6 +66,7 @@ import java.util.List; @RequiresFeature(PackageManager.FEATURE_APP_WIDGETS) public class AppWidgetManager { + /** * Activity action to launch from your {@link AppWidgetHost} activity when you want to * pick an AppWidget to display. The AppWidget picker activity will be launched. @@ -332,6 +336,17 @@ public class AppWidgetManager { public static final String ACTION_APPWIDGET_UPDATE = "android.appwidget.action.APPWIDGET_UPDATE"; /** + * A combination broadcast of APPWIDGET_ENABLED and APPWIDGET_UPDATE. + * Sent during boot time and when the host is binding the widget for the very first time + * + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @BroadcastBehavior(explicitOnly = true) + public static final String ACTION_APPWIDGET_ENABLE_AND_UPDATE = "android.appwidget.action" + + ".APPWIDGET_ENABLE_AND_UPDATE"; + + /** * Sent when the custom extras for an AppWidget change. * * <p class="note">This is a protected intent that can only be sent @@ -456,6 +471,8 @@ public class AppWidgetManager { public static final String ACTION_APPWIDGET_HOST_RESTORED = "android.appwidget.action.APPWIDGET_HOST_RESTORED"; + private static final String TAG = "AppWidgetManager"; + /** * An intent extra that contains multiple appWidgetIds. These are id values as * they were provided to the application during a recent restore from backup. It is @@ -511,6 +528,26 @@ public class AppWidgetManager { mPackageName = context.getOpPackageName(); mService = service; mDisplayMetrics = context.getResources().getDisplayMetrics(); + if (mService == null) { + return; + } + BackgroundThread.getExecutor().execute(() -> { + try { + mService.notifyProviderInheritance(getInstalledProvidersForPackage(mPackageName, + null) + .stream().filter(Objects::nonNull) + .map(info -> info.provider).filter(p -> { + try { + Class clazz = Class.forName(p.getClassName()); + return AppWidgetProvider.class.isAssignableFrom(clazz); + } catch (Exception e) { + return false; + } + }).toArray(ComponentName[]::new)); + } catch (Exception e) { + Log.e(TAG, "Nofity service of inheritance info", e); + } + }); } /** diff --git a/core/java/android/appwidget/AppWidgetProvider.java b/core/java/android/appwidget/AppWidgetProvider.java index a5d2198a6e17..3344ebc083a2 100644 --- a/core/java/android/appwidget/AppWidgetProvider.java +++ b/core/java/android/appwidget/AppWidgetProvider.java @@ -58,7 +58,12 @@ public class AppWidgetProvider extends BroadcastReceiver { // Protect against rogue update broadcasts (not really a security issue, // just filter bad broacasts out so subclasses are less likely to crash). String action = intent.getAction(); - if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { + if (AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE.equals(action)) { + this.onReceive(context, new Intent(intent) + .setAction(AppWidgetManager.ACTION_APPWIDGET_ENABLED)); + this.onReceive(context, new Intent(intent) + .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 0fd164de8ffb..085bfca158c1 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -918,7 +918,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } } - /** * Forwards BiometricStateListener to FingerprintService * @param listener new BiometricStateListener being added diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java index 16f9a12953f8..36e0dc35cb8e 100644 --- a/core/java/android/preference/SeekBarVolumizer.java +++ b/core/java/android/preference/SeekBarVolumizer.java @@ -16,7 +16,9 @@ package android.preference; +import android.Manifest; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.app.NotificationManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; @@ -35,6 +37,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.preference.VolumePreference.VolumeStore; +import android.provider.DeviceConfig; import android.provider.Settings; import android.provider.Settings.Global; import android.provider.Settings.System; @@ -44,6 +47,7 @@ import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import com.android.internal.annotations.GuardedBy; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.os.SomeArgs; import java.util.concurrent.TimeUnit; @@ -115,7 +119,6 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba private final int mMaxStreamVolume; private boolean mAffectedByRingerMode; private boolean mNotificationOrRing; - private final boolean mNotifAliasRing; private final Receiver mReceiver = new Receiver(); private Handler mHandler; @@ -158,6 +161,7 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba this(context, streamType, defaultUri, callback, true /* playSample */); } + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public SeekBarVolumizer( Context context, int streamType, @@ -180,8 +184,6 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba if (mNotificationOrRing) { mRingerMode = mAudioManager.getRingerModeInternal(); } - mNotifAliasRing = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); mZenMode = mNotificationManager.getZenMode(); if (hasAudioProductStrategies()) { @@ -288,7 +290,9 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba * so that when user attempts to slide the notification seekbar out of vibrate the * seekbar doesn't wrongly snap back to 0 when the streams aren't aliased */ - if (mNotifAliasRing || mStreamType == AudioManager.STREAM_RING + if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false) + || mStreamType == AudioManager.STREAM_RING || (mStreamType == AudioManager.STREAM_NOTIFICATION && mMuted)) { mSeekBar.setProgress(0, true); } @@ -365,7 +369,9 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba // set the time of stop volume if ((mStreamType == AudioManager.STREAM_VOICE_CALL || mStreamType == AudioManager.STREAM_RING - || (!mNotifAliasRing && mStreamType == AudioManager.STREAM_NOTIFICATION) + || (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false) + && mStreamType == AudioManager.STREAM_NOTIFICATION) || mStreamType == AudioManager.STREAM_ALARM)) { sStopVolumeTime = java.lang.System.currentTimeMillis(); } @@ -643,8 +649,10 @@ public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callba } private void updateVolumeSlider(int streamType, int streamValue) { - final boolean streamMatch = mNotifAliasRing && mNotificationOrRing - ? isNotificationOrRing(streamType) : streamType == mStreamType; + final boolean streamMatch = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false) + && mNotificationOrRing ? isNotificationOrRing(streamType) : + streamType == mStreamType; if (mSeekBar != null && streamMatch && streamValue != -1) { final boolean muted = mAudioManager.isStreamMute(mStreamType) || streamValue == 0; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c9fd1295a6e1..a955dbba97e7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9793,6 +9793,13 @@ public final class Settings { "fingerprint_side_fps_auth_downtime"; /** + * Whether or not a SFPS device is required to be interactive for auth to unlock the device. + * @hide + */ + public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED = + "sfps_require_screen_on_to_auth_enabled"; + + /** * Whether or not debugging is enabled. * @hide */ diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index df727e9b7d3d..48f732ec9f12 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -19,6 +19,7 @@ package android.service.voice; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -71,6 +72,8 @@ import com.android.internal.util.function.pooled.PooledLambda; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; @@ -143,6 +146,25 @@ public class VoiceInteractionSession implements KeyEvent.Callback, ComponentCall */ public static final int SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI = 1 << 7; + /** @hide */ + public static final int VOICE_INTERACTION_ACTIVITY_EVENT_START = 1; + /** @hide */ + public static final int VOICE_INTERACTION_ACTIVITY_EVENT_RESUME = 2; + /** @hide */ + public static final int VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE = 3; + /** @hide */ + public static final int VOICE_INTERACTION_ACTIVITY_EVENT_STOP = 4; + + /** @hide */ + @IntDef(prefix = { "VOICE_INTERACTION_ACTIVITY_EVENT_" }, value = { + VOICE_INTERACTION_ACTIVITY_EVENT_START, + VOICE_INTERACTION_ACTIVITY_EVENT_RESUME, + VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE, + VOICE_INTERACTION_ACTIVITY_EVENT_STOP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VoiceInteractionActivityEventType{} + final Context mContext; final HandlerCaller mHandlerCaller; diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 681693b1dbad..bd51f12539e8 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -18,6 +18,8 @@ package com.android.internal.app; import android.content.ComponentName; import android.content.Intent; +import android.hardware.soundtrigger.KeyphraseMetadata; +import android.hardware.soundtrigger.SoundTrigger; import android.media.AudioFormat; import android.media.permission.Identity; import android.os.Bundle; @@ -25,18 +27,17 @@ import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.SharedMemory; +import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; +import android.service.voice.IVoiceInteractionService; +import android.service.voice.IVoiceInteractionSession; +import android.service.voice.VisibleActivityInfo; import com.android.internal.app.IHotwordRecognitionStatusCallback; import com.android.internal.app.IVoiceActionCheckCallback; -import com.android.internal.app.IVoiceInteractionSessionShowCallback; -import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.IVoiceInteractionSessionListener; +import com.android.internal.app.IVoiceInteractionSessionShowCallback; import com.android.internal.app.IVoiceInteractionSoundTriggerSession; -import android.hardware.soundtrigger.KeyphraseMetadata; -import android.hardware.soundtrigger.SoundTrigger; -import android.service.voice.IVoiceInteractionService; -import android.service.voice.IVoiceInteractionSession; -import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; +import com.android.internal.app.IVoiceInteractor; interface IVoiceInteractionManagerService { void showSession(in Bundle sessionArgs, int flags); @@ -289,4 +290,14 @@ interface IVoiceInteractionManagerService { * Notifies when the session window is shown or hidden. */ void setSessionWindowVisible(in IBinder token, boolean visible); + + /** + * Notifies when the Activity lifecycle event changed. + * + * @param activityToken The token of activity. + * @param type The type of lifecycle event of the activity lifecycle. + */ + oneway void notifyActivityEventChanged( + in IBinder activityToken, + int type); } diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 6c689ff2b725..0f64f6da63f7 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -556,6 +556,11 @@ public final class SystemUiDeviceConfigFlags { "show_stop_button_for_user_allowlisted_apps"; /** + * (boolean) Whether to show notification volume control slider separate from ring. + */ + public static final String VOLUME_SEPARATE_NOTIFICATION = "volume_separate_notification"; + + /** * (boolean) Whether the clipboard overlay is enabled. */ public static final String CLIPBOARD_OVERLAY_ENABLED = "clipboard_overlay_enabled"; @@ -578,6 +583,11 @@ public final class SystemUiDeviceConfigFlags { */ public static final String CLIPBOARD_OVERLAY_SHOW_ACTIONS = "clipboard_overlay_show_actions"; + /** + * (boolean) Whether to combine the broadcasts APPWIDGET_ENABLED and APPWIDGET_UPDATE + */ + public static final String COMBINED_BROADCAST_ENABLED = "combined_broadcast_enabled"; + private SystemUiDeviceConfigFlags() { } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index f7467b5a9f86..7787b7c61593 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -142,6 +142,7 @@ <protected-broadcast android:name="android.appwidget.action.APPWIDGET_ENABLED" /> <protected-broadcast android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED" /> <protected-broadcast android:name="android.appwidget.action.APPWIDGET_RESTORED" /> + <protected-broadcast android:name="android.appwidget.action.APPWIDGET_ENABLE_AND_UPDATE" /> <protected-broadcast android:name="android.os.action.SETTING_RESTORED" /> diff --git a/core/res/res/values/bools.xml b/core/res/res/values/bools.xml index 988303ea7bc5..4b27bf2849fb 100644 --- a/core/res/res/values/bools.xml +++ b/core/res/res/values/bools.xml @@ -31,5 +31,4 @@ lockscreen, setting this to true should come with customized drawables. --> <bool name="use_lock_pattern_drawable">false</bool> <bool name="resolver_landscape_phone">true</bool> - <bool name="system_server_plays_face_haptics">true</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index aa9a9499ed97..07e05c6a830e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2070,10 +2070,6 @@ STREAM_MUSIC as if it's on TV platform. --> <bool name="config_single_volume">false</bool> - <!-- Flag indicating whether notification and ringtone volumes - are controlled together (aliasing is true) or not. --> - <bool name="config_alias_ring_notif_stream_types">true</bool> - <!-- The number of volume steps for the notification stream --> <integer name="config_audio_notif_vol_steps">7</integer> @@ -2491,8 +2487,10 @@ assistant activities (ACTIVITY_TYPE_ASSISTANT) --> <bool name="config_dismissDreamOnActivityStart">false</bool> - <!-- The prefix of dream component names that are loggable. If empty, logs "other" for all. --> - <string name="config_loggable_dream_prefix" translatable="false"></string> + <!-- The prefixes of dream component names that are loggable. + Matched against ComponentName#flattenToString() for dream components. + If empty, logs "other" for all. --> + <string-array name="config_loggable_dream_prefixes"></string-array> <!-- ComponentName of a dream to show whenever the system would otherwise have gone to sleep. When the PowerManager is asked to go to sleep, it will instead @@ -4967,6 +4965,10 @@ <!-- If face auth sends the user directly to home/last open app, or stays on keyguard --> <bool name="config_faceAuthDismissesKeyguard">true</bool> + <!-- Default value for whether a SFPS device is required to be interactive for fingerprint auth + to unlock the device. --> + <bool name="config_requireScreenOnToAuthEnabled">false</bool> + <!-- The component name for the default profile supervisor, which can be set as a profile owner even after user setup is complete. The defined component should be used for supervision purposes only. The component must be part of a system app. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 7a7b43a3632a..8e7da4a5eac3 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -278,7 +278,6 @@ <java-symbol type="attr" name="autofillSaveCustomSubtitleMaxHeight"/> <java-symbol type="bool" name="action_bar_embed_tabs" /> <java-symbol type="bool" name="action_bar_expanded_action_views_exclusive" /> - <java-symbol type="bool" name="config_alias_ring_notif_stream_types" /> <java-symbol type="integer" name="config_audio_notif_vol_default" /> <java-symbol type="integer" name="config_audio_notif_vol_steps" /> <java-symbol type="integer" name="config_audio_ring_vol_default" /> @@ -2245,7 +2244,7 @@ <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" /> <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" /> <java-symbol type="integer" name="config_minDreamOverlayDurationMs" /> - <java-symbol type="string" name="config_loggable_dream_prefix" /> + <java-symbol type="array" name="config_loggable_dream_prefixes" /> <java-symbol type="string" name="config_dozeComponent" /> <java-symbol type="string" name="enable_explore_by_touch_warning_title" /> <java-symbol type="string" name="enable_explore_by_touch_warning_message" /> @@ -2723,6 +2722,7 @@ <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" /> <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" /> <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" /> + <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" /> <!-- Face config --> <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" /> @@ -4861,6 +4861,4 @@ <java-symbol type="id" name="language_picker_header" /> <java-symbol type="dimen" name="status_bar_height_default" /> - - <java-symbol type="bool" name="system_server_plays_face_haptics" /> </resources> 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 1d513e444050..16760e26b3f1 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -1873,6 +1873,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onActivityPreCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } synchronized (mLock) { final IBinder activityToken = activity.getActivityToken(); final IBinder initialTaskFragmentToken = @@ -1904,6 +1909,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onActivityPostCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } // Calling after Activity#onCreate is complete to allow the app launch something // first. In case of a configured placeholder activity we want to make sure // that we don't launch it if an activity itself already requested something to be @@ -1921,6 +1931,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onActivityConfigurationChanged(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } synchronized (mLock) { final TransactionRecord transactionRecord = mTransactionManager .startNewTransaction(); @@ -1934,6 +1949,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void onActivityPostDestroyed(@NonNull Activity activity) { + if (activity.isChild()) { + // Skip Activity that is child of another Activity (ActivityGroup) because it's + // window will just be a child of the parent Activity window. + return; + } synchronized (mLock) { SplitController.this.onActivityDestroyed(activity); } @@ -1969,7 +1989,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (who instanceof Activity) { // We will check if the new activity should be split with the activity that launched // it. - launchingActivity = (Activity) who; + final Activity activity = (Activity) who; + // For Activity that is child of another Activity (ActivityGroup), treat the parent + // Activity as the launching one because it's window will just be a child of the + // parent Activity window. + launchingActivity = activity.isChild() ? activity.getParent() : activity; if (isInPictureInPicture(launchingActivity)) { // We don't embed activity when it is in PIP. return super.onStartActivity(who, intent, options); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 79603233ae14..362f1fa096cc 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -227,7 +227,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( secondaryActivity); TaskFragmentContainer containerToAvoid = primaryContainer; - if (curSecondaryContainer != null + if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) { // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below // the primary TaskFragment. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS new file mode 100644 index 000000000000..7237d2bde39f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-modules splitscreen owner +chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index f4efc374ecc2..1c28c3d58ccb 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -6,3 +6,4 @@ pablogamito@google.com lbill@google.com madym@google.com hwwang@google.com +chenghsiuchang@google.com diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 453a71327b50..8946516af8a2 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -122,6 +122,7 @@ public class SecureSettings { Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, Settings.Secure.ACTIVE_UNLOCK_ON_WAKE, Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT, Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index a39735ffe2c7..cbf79530a6b6 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -177,6 +177,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR); + VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 2f5b5f456d0b..6d542700fceb 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -125,6 +125,7 @@ android_library { "jsr330", "lottie", "LowLightDreamLib", + "motion_tool_lib", ], manifest: "AndroidManifest.xml", @@ -229,6 +230,7 @@ android_library { "jsr330", "WindowManager-Shell", "LowLightDreamLib", + "motion_tool_lib", ], libs: [ "android.test.runner", diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 006b260434bc..9add32c6ee0a 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -18,6 +18,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/dream_overlay_status_bar" + android:visibility="invisible" android:layout_width="match_parent" android:layout_height="@dimen/dream_overlay_status_bar_height" android:paddingEnd="@dimen/dream_overlay_status_bar_margin" diff --git a/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml new file mode 100644 index 000000000000..aab914f19878 --- /dev/null +++ b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<!-- Intended for wide devices that are currently oriented with a lot of available height, + such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied + to wide devices that are shorter in height, like foldables. --> +<resources> + <!-- Space between status view and notification shelf --> + <dimen name="keyguard_status_view_bottom_margin">35dp</dimen> + <dimen name="keyguard_clock_top_margin">40dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml index 347cf2965c77..d9df3373bef1 100644 --- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml @@ -17,8 +17,6 @@ <resources> <dimen name="notification_panel_margin_horizontal">48dp</dimen> <dimen name="status_view_margin_horizontal">62dp</dimen> - <dimen name="keyguard_clock_top_margin">40dp</dimen> - <dimen name="keyguard_status_view_bottom_margin">40dp</dimen> <dimen name="bouncer_user_switcher_y_trans">20dp</dimen> <!-- qs_tiles_page_horizontal_margin should be margin / 2, otherwise full space between two diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml new file mode 100644 index 000000000000..97ead01669a9 --- /dev/null +++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<!-- Intended for wide devices that are currently oriented with a lot of available height, + such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied + to wide devices that are shorter in height, like foldables. --> +<resources> + <!-- Space between status view and notification shelf --> + <dimen name="keyguard_status_view_bottom_margin">70dp</dimen> + <dimen name="keyguard_clock_top_margin">80dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml index 3d8da8a6c3e8..17f82b50d7be 100644 --- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml @@ -21,8 +21,6 @@ for different hardware and product builds. --> <resources> <dimen name="status_view_margin_horizontal">124dp</dimen> - <dimen name="keyguard_clock_top_margin">80dp</dimen> - <dimen name="keyguard_status_view_bottom_margin">80dp</dimen> <dimen name="bouncer_user_switcher_y_trans">200dp</dimen> <dimen name="large_screen_shade_header_left_padding">24dp</dimen> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 93982cb2c5b9..ce9829b318cd 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -743,6 +743,17 @@ <integer name="complicationRestoreMs">1000</integer> + <!-- Duration in milliseconds of the dream in un-blur animation. --> + <integer name="config_dreamOverlayInBlurDurationMs">249</integer> + <!-- Delay in milliseconds of the dream in un-blur animation. --> + <integer name="config_dreamOverlayInBlurDelayMs">133</integer> + <!-- Duration in milliseconds of the dream in complications fade-in animation. --> + <integer name="config_dreamOverlayInComplicationsDurationMs">282</integer> + <!-- Delay in milliseconds of the dream in top complications fade-in animation. --> + <integer name="config_dreamOverlayInTopComplicationsDelayMs">216</integer> + <!-- Delay in milliseconds of the dream in bottom complications fade-in animation. --> + <integer name="config_dreamOverlayInBottomComplicationsDelayMs">299</integer> + <!-- Icons that don't show in a collapsed non-keyguard statusbar --> <string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false"> <item>@*android:string/status_bar_volume</item> @@ -783,4 +794,8 @@ <item>@color/dream_overlay_aqi_very_unhealthy</item> <item>@color/dream_overlay_aqi_hazardous</item> </integer-array> + + <!-- Whether the device should display hotspot UI. If true, UI will display only when tethering + is available. If false, UI will never show regardless of tethering availability" --> + <bool name="config_show_wifi_tethering">true</bool> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java index 8aa3abac831f..a14f97128662 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java +++ b/packages/SystemUI/shared/src/com/android/systemui/navigationbar/buttons/KeyButtonRipple.java @@ -291,8 +291,10 @@ public class KeyButtonRipple extends Drawable { } private void endAnimations(String reason, boolean cancel) { - Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel); - Trace.endSection(); + if (Trace.isEnabled()) { + Trace.instant(Trace.TRACE_TAG_APP, + "KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel); + } mVisible = false; mTmpArray.addAll(mRunningAnimations); int size = mTmpArray.size(); @@ -502,20 +504,23 @@ public class KeyButtonRipple extends Drawable { @Override public void onAnimationStart(Animator animation) { - Trace.beginSection("KeyButtonRipple.start." + mName); - Trace.endSection(); + if (Trace.isEnabled()) { + Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.start." + mName); + } } @Override public void onAnimationCancel(Animator animation) { - Trace.beginSection("KeyButtonRipple.cancel." + mName); - Trace.endSection(); + if (Trace.isEnabled()) { + Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.cancel." + mName); + } } @Override public void onAnimationEnd(Animator animation) { - Trace.beginSection("KeyButtonRipple.end." + mName); - Trace.endSection(); + if (Trace.isEnabled()) { + Trace.instant(Trace.TRACE_TAG_APP, "KeyButtonRipple.end." + mName); + } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 3961438ff591..ca780c8dd3c9 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -188,13 +188,10 @@ class DefaultClockController( dozeFraction: Float, foldFraction: Float, ) : ClockAnimations { - private var foldState = AnimationState(0f) - private var dozeState = AnimationState(0f) + private val dozeState = AnimationState(dozeFraction) + private val foldState = AnimationState(foldFraction) init { - dozeState = AnimationState(dozeFraction) - foldState = AnimationState(foldFraction) - if (foldState.isActive) { clocks.forEach { it.animateFoldAppear(false) } } else { @@ -235,7 +232,7 @@ class DefaultClockController( private class AnimationState( var fraction: Float, ) { - var isActive: Boolean = fraction < 0.5f + var isActive: Boolean = fraction > 0.5f fun update(newFraction: Float): Pair<Boolean, Boolean> { if (newFraction == fraction) { return Pair(isActive, false) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt index 1550ab3bed63..2dc7a280e423 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -12,8 +12,16 @@ * WITHOUT 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.shared.system; +package com.android.systemui.shared.keyguard.shared.model -parcelable RemoteTransitionCompat; +/** + * Collection of all supported "slots", placements where keyguard quick affordances can appear on + * the lock screen. + */ +object KeyguardQuickAffordanceSlots { + const val SLOT_ID_BOTTOM_START = "bottom_start" + const val SLOT_ID_BOTTOM_END = "bottom_end" +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index b99b72bb4275..1c532fe7a529 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -24,7 +24,6 @@ import android.os.UserHandle; import android.view.MotionEvent; import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.system.RemoteTransitionCompat; /** * Temporary callbacks into SystemUI. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java deleted file mode 100644 index 37e706a9a4c9..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.shared.system; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_OLD_NONE; -import static android.view.WindowManager.TRANSIT_OPEN; -import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.TransitionOldType; -import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; - -import android.annotation.SuppressLint; -import android.app.IApplicationThread; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.Log; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.window.IRemoteTransition; -import android.window.IRemoteTransitionFinishedCallback; -import android.window.RemoteTransition; -import android.window.TransitionInfo; - -import com.android.wm.shell.util.CounterRotator; - -/** - * @see RemoteAnimationAdapter - */ -public class RemoteAnimationAdapterCompat { - - private final RemoteAnimationAdapter mWrapped; - private final RemoteTransitionCompat mRemoteTransition; - - public RemoteAnimationAdapterCompat(RemoteAnimationRunnerCompat runner, long duration, - long statusBarTransitionDelay, IApplicationThread appThread) { - mWrapped = new RemoteAnimationAdapter(wrapRemoteAnimationRunner(runner), duration, - statusBarTransitionDelay); - mRemoteTransition = buildRemoteTransition(runner, appThread); - } - - public RemoteAnimationAdapter getWrapped() { - return mWrapped; - } - - /** Helper to just build a remote transition. Use this if the legacy adapter isn't needed. */ - public static RemoteTransitionCompat buildRemoteTransition(RemoteAnimationRunnerCompat runner, - IApplicationThread appThread) { - return new RemoteTransitionCompat( - new RemoteTransition(wrapRemoteTransition(runner), appThread)); - } - - public RemoteTransitionCompat getRemoteTransition() { - return mRemoteTransition; - } - - /** Wraps a RemoteAnimationRunnerCompat in an IRemoteAnimationRunner. */ - public static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner( - final RemoteAnimationRunnerCompat remoteAnimationAdapter) { - return new IRemoteAnimationRunner.Stub() { - @Override - public void onAnimationStart(@TransitionOldType int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - final IRemoteAnimationFinishedCallback finishedCallback) { - final Runnable animationFinishedCallback = new Runnable() { - @Override - public void run() { - try { - finishedCallback.onAnimationFinished(); - } catch (RemoteException e) { - Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" - + " finished callback", e); - } - } - }; - remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers, - nonApps, animationFinishedCallback); - } - - @Override - public void onAnimationCancelled(boolean isKeyguardOccluded) { - remoteAnimationAdapter.onAnimationCancelled(); - } - }; - } - - private static IRemoteTransition.Stub wrapRemoteTransition( - final RemoteAnimationRunnerCompat remoteAnimationAdapter) { - return new IRemoteTransition.Stub() { - final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); - - @Override - public void startAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, - IRemoteTransitionFinishedCallback finishCallback) { - final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); - final RemoteAnimationTarget[] apps = - RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); - final RemoteAnimationTarget[] wallpapers = - RemoteAnimationTargetCompat.wrapNonApps( - info, true /* wallpapers */, t, leashMap); - final RemoteAnimationTarget[] nonApps = - RemoteAnimationTargetCompat.wrapNonApps( - info, false /* wallpapers */, t, leashMap); - - // TODO(b/177438007): Move this set-up logic into launcher's animation impl. - boolean isReturnToHome = false; - TransitionInfo.Change launcherTask = null; - TransitionInfo.Change wallpaper = null; - int launcherLayer = 0; - int rotateDelta = 0; - float displayW = 0; - float displayH = 0; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - // skip changes that we didn't wrap - if (!leashMap.containsKey(change.getLeash())) continue; - if (change.getTaskInfo() != null - && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME) { - isReturnToHome = change.getMode() == TRANSIT_OPEN - || change.getMode() == TRANSIT_TO_FRONT; - launcherTask = change; - launcherLayer = info.getChanges().size() - i; - } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - wallpaper = change; - } - if (change.getParent() == null && change.getEndRotation() >= 0 - && change.getEndRotation() != change.getStartRotation()) { - rotateDelta = change.getEndRotation() - change.getStartRotation(); - displayW = change.getEndAbsBounds().width(); - displayH = change.getEndAbsBounds().height(); - } - } - - // Prepare for rotation if there is one - final CounterRotator counterLauncher = new CounterRotator(); - final CounterRotator counterWallpaper = new CounterRotator(); - if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) { - counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(), - rotateDelta, displayW, displayH); - if (counterLauncher.getSurface() != null) { - t.setLayer(counterLauncher.getSurface(), launcherLayer); - } - } - - if (isReturnToHome) { - if (counterLauncher.getSurface() != null) { - t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3); - } - // Need to "boost" the closing things since that's what launcher expects. - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - final SurfaceControl leash = leashMap.get(change.getLeash()); - // skip changes that we didn't wrap - if (leash == null) continue; - final int mode = info.getChanges().get(i).getMode(); - // Only deal with independent layers - if (!TransitionInfo.isIndependent(change, info)) continue; - if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { - t.setLayer(leash, info.getChanges().size() * 3 - i); - counterLauncher.addChild(t, leash); - } - } - // Make wallpaper visible immediately since launcher apparently won't do this. - for (int i = wallpapers.length - 1; i >= 0; --i) { - t.show(wallpapers[i].leash); - t.setAlpha(wallpapers[i].leash, 1.f); - } - } else { - if (launcherTask != null) { - counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash())); - } - if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) { - counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(), - rotateDelta, displayW, displayH); - if (counterWallpaper.getSurface() != null) { - t.setLayer(counterWallpaper.getSurface(), -1); - counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash())); - } - } - } - t.apply(); - - final Runnable animationFinishedCallback = new Runnable() { - @Override - @SuppressLint("NewApi") - public void run() { - final SurfaceControl.Transaction finishTransaction = - new SurfaceControl.Transaction(); - counterLauncher.cleanUp(finishTransaction); - counterWallpaper.cleanUp(finishTransaction); - // Release surface references now. This is apparently to free GPU memory - // while doing quick operations (eg. during CTS). - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - info.getChanges().get(i).getLeash().release(); - } - // Don't release here since launcher might still be using them. Instead - // let launcher release them (eg. via RemoteAnimationTargets) - leashMap.clear(); - try { - finishCallback.onTransitionFinished(null /* wct */, finishTransaction); - } catch (RemoteException e) { - Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" - + " finished callback", e); - } - } - }; - synchronized (mFinishRunnables) { - mFinishRunnables.put(token, animationFinishedCallback); - } - // TODO(bc-unlcok): Pass correct transit type. - remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE, - apps, wallpapers, nonApps, () -> { - synchronized (mFinishRunnables) { - if (mFinishRunnables.remove(token) == null) return; - } - animationFinishedCallback.run(); - }); - } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) { - // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt - // to legacy cancel. - final Runnable finishRunnable; - synchronized (mFinishRunnables) { - finishRunnable = mFinishRunnables.remove(mergeTarget); - } - if (finishRunnable == null) return; - remoteAnimationAdapter.onAnimationCancelled(); - finishRunnable.run(); - } - }; - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java deleted file mode 100644 index ab55037159ef..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.shared.system; - -import android.view.RemoteAnimationDefinition; - -/** - * @see RemoteAnimationDefinition - */ -public class RemoteAnimationDefinitionCompat { - - private final RemoteAnimationDefinition mWrapped = new RemoteAnimationDefinition(); - - public void addRemoteAnimation(int transition, RemoteAnimationAdapterCompat adapter) { - mWrapped.addRemoteAnimation(transition, adapter.getWrapped()); - } - - public void addRemoteAnimation(int transition, int activityTypeFilter, - RemoteAnimationAdapterCompat adapter) { - mWrapped.addRemoteAnimation(transition, activityTypeFilter, adapter.getWrapped()); - } - - public RemoteAnimationDefinition getWrapped() { - return mWrapped; - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java index 5809c8124946..93c807352521 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java @@ -16,12 +16,197 @@ package com.android.systemui.shared.system; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OLD_NONE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; import android.view.WindowManager; +import android.view.WindowManager.TransitionOldType; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionInfo; + +import com.android.wm.shell.util.CounterRotator; + +public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner.Stub { -public interface RemoteAnimationRunnerCompat { - void onAnimationStart(@WindowManager.TransitionOldType int transit, + public abstract void onAnimationStart(@WindowManager.TransitionOldType int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback); - void onAnimationCancelled(); + + @Override + public final void onAnimationStart(@TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + + onAnimationStart(transit, apps, wallpapers, + nonApps, () -> { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" + + " finished callback", e); + } + }); + } + + public IRemoteTransition toRemoteTransition() { + return new IRemoteTransition.Stub() { + final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); + + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) { + final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>(); + final RemoteAnimationTarget[] apps = + RemoteAnimationTargetCompat.wrapApps(info, t, leashMap); + final RemoteAnimationTarget[] wallpapers = + RemoteAnimationTargetCompat.wrapNonApps( + info, true /* wallpapers */, t, leashMap); + final RemoteAnimationTarget[] nonApps = + RemoteAnimationTargetCompat.wrapNonApps( + info, false /* wallpapers */, t, leashMap); + + // TODO(b/177438007): Move this set-up logic into launcher's animation impl. + boolean isReturnToHome = false; + TransitionInfo.Change launcherTask = null; + TransitionInfo.Change wallpaper = null; + int launcherLayer = 0; + int rotateDelta = 0; + float displayW = 0; + float displayH = 0; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + // skip changes that we didn't wrap + if (!leashMap.containsKey(change.getLeash())) continue; + if (change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME) { + isReturnToHome = change.getMode() == TRANSIT_OPEN + || change.getMode() == TRANSIT_TO_FRONT; + launcherTask = change; + launcherLayer = info.getChanges().size() - i; + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + wallpaper = change; + } + if (change.getParent() == null && change.getEndRotation() >= 0 + && change.getEndRotation() != change.getStartRotation()) { + rotateDelta = change.getEndRotation() - change.getStartRotation(); + displayW = change.getEndAbsBounds().width(); + displayH = change.getEndAbsBounds().height(); + } + } + + // Prepare for rotation if there is one + final CounterRotator counterLauncher = new CounterRotator(); + final CounterRotator counterWallpaper = new CounterRotator(); + if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) { + counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(), + rotateDelta, displayW, displayH); + if (counterLauncher.getSurface() != null) { + t.setLayer(counterLauncher.getSurface(), launcherLayer); + } + } + + if (isReturnToHome) { + if (counterLauncher.getSurface() != null) { + t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3); + } + // Need to "boost" the closing things since that's what launcher expects. + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = leashMap.get(change.getLeash()); + // skip changes that we didn't wrap + if (leash == null) continue; + final int mode = info.getChanges().get(i).getMode(); + // Only deal with independent layers + if (!TransitionInfo.isIndependent(change, info)) continue; + if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + t.setLayer(leash, info.getChanges().size() * 3 - i); + counterLauncher.addChild(t, leash); + } + } + // Make wallpaper visible immediately since launcher apparently won't do this. + for (int i = wallpapers.length - 1; i >= 0; --i) { + t.show(wallpapers[i].leash); + t.setAlpha(wallpapers[i].leash, 1.f); + } + } else { + if (launcherTask != null) { + counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash())); + } + if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) { + counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(), + rotateDelta, displayW, displayH); + if (counterWallpaper.getSurface() != null) { + t.setLayer(counterWallpaper.getSurface(), -1); + counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash())); + } + } + } + t.apply(); + + final Runnable animationFinishedCallback = () -> { + final SurfaceControl.Transaction finishTransaction = + new SurfaceControl.Transaction(); + counterLauncher.cleanUp(finishTransaction); + counterWallpaper.cleanUp(finishTransaction); + // Release surface references now. This is apparently to free GPU memory + // while doing quick operations (eg. during CTS). + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + info.getChanges().get(i).getLeash().release(); + } + // Don't release here since launcher might still be using them. Instead + // let launcher release them (eg. via RemoteAnimationTargets) + leashMap.clear(); + try { + finishCallback.onTransitionFinished(null /* wct */, finishTransaction); + } catch (RemoteException e) { + Log.e("ActivityOptionsCompat", "Failed to call app controlled animation" + + " finished callback", e); + } + }; + synchronized (mFinishRunnables) { + mFinishRunnables.put(token, animationFinishedCallback); + } + // TODO(bc-unlcok): Pass correct transit type. + onAnimationStart(TRANSIT_OLD_NONE, + apps, wallpapers, nonApps, () -> { + synchronized (mFinishRunnables) { + if (mFinishRunnables.remove(token) == null) return; + } + animationFinishedCallback.run(); + }); + } + + @Override + public void mergeAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt + // to legacy cancel. + final Runnable finishRunnable; + synchronized (mFinishRunnables) { + finishRunnable = mFinishRunnables.remove(mergeTarget); + } + if (finishRunnable == null) return; + onAnimationCancelled(false /* isKeyguardOccluded */); + finishRunnable.run(); + } + }; + } }
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index d6655a74219c..d4d3d2579b10 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -18,29 +18,22 @@ package com.android.systemui.shared.system; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.window.TransitionFilter.CONTAINER_ORDER_TOP; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; -import android.content.ComponentName; import android.graphics.Rect; import android.os.IBinder; -import android.os.Parcelable; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; @@ -53,72 +46,23 @@ import android.window.IRemoteTransitionFinishedCallback; import android.window.PictureInPictureSurfaceTransaction; import android.window.RemoteTransition; import android.window.TaskSnapshot; -import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.DataClass; import com.android.systemui.shared.recents.model.ThumbnailData; import java.util.ArrayList; -import java.util.concurrent.Executor; /** - * Wrapper to expose RemoteTransition (shell transitions) to Launcher. - * - * @see IRemoteTransition - * @see TransitionFilter + * Helper class to build {@link RemoteTransition} objects */ -@DataClass -public class RemoteTransitionCompat implements Parcelable { +public class RemoteTransitionCompat { private static final String TAG = "RemoteTransitionCompat"; - @NonNull final RemoteTransition mTransition; - @Nullable TransitionFilter mFilter = null; - - RemoteTransitionCompat(RemoteTransition transition) { - mTransition = transition; - } - - public RemoteTransitionCompat(@NonNull RemoteTransitionRunner runner, - @NonNull Executor executor, @Nullable IApplicationThread appThread) { - IRemoteTransition remote = new IRemoteTransition.Stub() { - @Override - public void startAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, - IRemoteTransitionFinishedCallback finishedCallback) { - final Runnable finishAdapter = () -> { - try { - finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); - } catch (RemoteException e) { - Log.e(TAG, "Failed to call transition finished callback", e); - } - }; - executor.execute(() -> runner.startAnimation(transition, info, t, finishAdapter)); - } - - @Override - public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishedCallback) { - final Runnable finishAdapter = () -> { - try { - finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); - } catch (RemoteException e) { - Log.e(TAG, "Failed to call transition finished callback", e); - } - }; - executor.execute(() -> runner.mergeAnimation(transition, info, t, mergeTarget, - finishAdapter)); - } - }; - mTransition = new RemoteTransition(remote, appThread); - } - /** Constructor specifically for recents animation */ - public RemoteTransitionCompat(RecentsAnimationListener recents, + public static RemoteTransition newRemoteTransition(RecentsAnimationListener recents, RecentsAnimationControllerCompat controller, IApplicationThread appThread) { IRemoteTransition remote = new IRemoteTransition.Stub() { final RecentsControllerWrap mRecentsSession = new RecentsControllerWrap(); @@ -193,25 +137,7 @@ public class RemoteTransitionCompat implements Parcelable { mRecentsSession.commitTasksAppearedIfNeeded(recents); } }; - mTransition = new RemoteTransition(remote, appThread); - } - - /** Adds a filter check that restricts this remote transition to home open transitions. */ - public void addHomeOpenCheck(ComponentName homeActivity) { - if (mFilter == null) { - mFilter = new TransitionFilter(); - } - // No need to handle the transition that also dismisses keyguard. - mFilter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; - mFilter.mRequirements = - new TransitionFilter.Requirement[]{new TransitionFilter.Requirement(), - new TransitionFilter.Requirement()}; - mFilter.mRequirements[0].mActivityType = ACTIVITY_TYPE_HOME; - mFilter.mRequirements[0].mTopActivity = homeActivity; - mFilter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - mFilter.mRequirements[0].mOrder = CONTAINER_ORDER_TOP; - mFilter.mRequirements[1].mActivityType = ACTIVITY_TYPE_STANDARD; - mFilter.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK}; + return new RemoteTransition(remote, appThread); } /** @@ -505,161 +431,4 @@ public class RemoteTransitionCompat implements Parcelable { @Override public void animateNavigationBarToApp(long duration) { } } - - - - // Code below generated by codegen v1.0.23. - // - // DO NOT MODIFY! - // CHECKSTYLE:OFF Generated code - // - // To regenerate run: - // $ codegen $ANDROID_BUILD_TOP/frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java - // - // To exclude the generated code from IntelliJ auto-formatting enable (one-time): - // Settings > Editor > Code Style > Formatter Control - //@formatter:off - - - @DataClass.Generated.Member - /* package-private */ RemoteTransitionCompat( - @NonNull RemoteTransition transition, - @Nullable TransitionFilter filter) { - this.mTransition = transition; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mTransition); - this.mFilter = filter; - - // onConstructed(); // You can define this method to get a callback - } - - @DataClass.Generated.Member - public @NonNull RemoteTransition getTransition() { - return mTransition; - } - - @DataClass.Generated.Member - public @Nullable TransitionFilter getFilter() { - return mFilter; - } - - @Override - @DataClass.Generated.Member - public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { - // You can override field parcelling by defining methods like: - // void parcelFieldName(Parcel dest, int flags) { ... } - - byte flg = 0; - if (mFilter != null) flg |= 0x2; - dest.writeByte(flg); - dest.writeTypedObject(mTransition, flags); - if (mFilter != null) dest.writeTypedObject(mFilter, flags); - } - - @Override - @DataClass.Generated.Member - public int describeContents() { return 0; } - - /** @hide */ - @SuppressWarnings({"unchecked", "RedundantCast"}) - @DataClass.Generated.Member - protected RemoteTransitionCompat(@NonNull android.os.Parcel in) { - // You can override field unparcelling by defining methods like: - // static FieldType unparcelFieldName(Parcel in) { ... } - - byte flg = in.readByte(); - RemoteTransition transition = (RemoteTransition) in.readTypedObject(RemoteTransition.CREATOR); - TransitionFilter filter = (flg & 0x2) == 0 ? null : (TransitionFilter) in.readTypedObject(TransitionFilter.CREATOR); - - this.mTransition = transition; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mTransition); - this.mFilter = filter; - - // onConstructed(); // You can define this method to get a callback - } - - @DataClass.Generated.Member - public static final @NonNull Parcelable.Creator<RemoteTransitionCompat> CREATOR - = new Parcelable.Creator<RemoteTransitionCompat>() { - @Override - public RemoteTransitionCompat[] newArray(int size) { - return new RemoteTransitionCompat[size]; - } - - @Override - public RemoteTransitionCompat createFromParcel(@NonNull android.os.Parcel in) { - return new RemoteTransitionCompat(in); - } - }; - - /** - * A builder for {@link RemoteTransitionCompat} - */ - @SuppressWarnings("WeakerAccess") - @DataClass.Generated.Member - public static class Builder { - - private @NonNull RemoteTransition mTransition; - private @Nullable TransitionFilter mFilter; - - private long mBuilderFieldsSet = 0L; - - public Builder( - @NonNull RemoteTransition transition) { - mTransition = transition; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mTransition); - } - - @DataClass.Generated.Member - public @NonNull Builder setTransition(@NonNull RemoteTransition value) { - checkNotUsed(); - mBuilderFieldsSet |= 0x1; - mTransition = value; - return this; - } - - @DataClass.Generated.Member - public @NonNull Builder setFilter(@NonNull TransitionFilter value) { - checkNotUsed(); - mBuilderFieldsSet |= 0x2; - mFilter = value; - return this; - } - - /** Builds the instance. This builder should not be touched after calling this! */ - public @NonNull RemoteTransitionCompat build() { - checkNotUsed(); - mBuilderFieldsSet |= 0x4; // Mark builder used - - if ((mBuilderFieldsSet & 0x2) == 0) { - mFilter = null; - } - RemoteTransitionCompat o = new RemoteTransitionCompat( - mTransition, - mFilter); - return o; - } - - private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x4) != 0) { - throw new IllegalStateException( - "This Builder should not be reused. Use a new Builder instance instead"); - } - } - } - - @DataClass.Generated( - time = 1629321609807L, - codegenVersion = "1.0.23", - sourceFile = "frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java", - inputSignatures = "private static final java.lang.String TAG\nfinal @android.annotation.NonNull android.window.RemoteTransition mTransition\n @android.annotation.Nullable android.window.TransitionFilter mFilter\npublic void addHomeOpenCheck(android.content.ComponentName)\nclass RemoteTransitionCompat extends java.lang.Object implements [android.os.Parcelable]\nprivate com.android.systemui.shared.system.RecentsAnimationControllerCompat mWrapped\nprivate android.window.IRemoteTransitionFinishedCallback mFinishCB\nprivate android.window.WindowContainerToken mPausingTask\nprivate android.window.WindowContainerToken mPipTask\nprivate android.window.TransitionInfo mInfo\nprivate android.view.SurfaceControl mOpeningLeash\nprivate android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl> mLeashMap\nprivate android.window.PictureInPictureSurfaceTransaction mPipTransaction\nprivate android.os.IBinder mTransition\n void setup(com.android.systemui.shared.system.RecentsAnimationControllerCompat,android.window.TransitionInfo,android.window.IRemoteTransitionFinishedCallback,android.window.WindowContainerToken,android.window.WindowContainerToken,android.util.ArrayMap<android.view.SurfaceControl,android.view.SurfaceControl>,android.os.IBinder)\n @android.annotation.SuppressLint boolean merge(android.window.TransitionInfo,android.view.SurfaceControl.Transaction,com.android.systemui.shared.system.RecentsAnimationListener)\npublic @java.lang.Override com.android.systemui.shared.recents.model.ThumbnailData screenshotTask(int)\npublic @java.lang.Override void setInputConsumerEnabled(boolean)\npublic @java.lang.Override void setAnimationTargetsBehindSystemBars(boolean)\npublic @java.lang.Override void hideCurrentInputMethod()\npublic @java.lang.Override void setFinishTaskTransaction(int,android.window.PictureInPictureSurfaceTransaction,android.view.SurfaceControl)\npublic @java.lang.Override @android.annotation.SuppressLint void finish(boolean,boolean)\npublic @java.lang.Override void setDeferCancelUntilNextTransition(boolean,boolean)\npublic @java.lang.Override void cleanupScreenshot()\npublic @java.lang.Override void setWillFinishToHome(boolean)\npublic @java.lang.Override boolean removeTask(int)\npublic @java.lang.Override void detachNavigationBarFromApp(boolean)\npublic @java.lang.Override void animateNavigationBarToApp(long)\nclass RecentsControllerWrap extends com.android.systemui.shared.system.RecentsAnimationControllerCompat implements []\n@com.android.internal.util.DataClass") - @Deprecated - private void __metadata() {} - - - //@formatter:on - // End of generated code - } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java deleted file mode 100644 index accc456c4209..000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionRunner.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.shared.system; - -import android.os.IBinder; -import android.view.SurfaceControl; -import android.window.TransitionInfo; - -/** Interface for something that runs a remote transition animation. */ -public interface RemoteTransitionRunner { - /** - * Starts a transition animation. Once complete, the implementation should call - * `finishCallback`. - */ - void startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, - Runnable finishCallback); - - /** - * Attempts to merge a transition into the currently-running animation. If merge is not - * possible/supported, this should do nothing. Otherwise, the implementation should call - * `finishCallback` immediately to indicate that it merged the transition. - * - * @param transition The transition that wants to be merged into the running animation. - * @param mergeTarget The transition to merge into (that this runner is currently animating). - */ - default void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, Runnable finishCallback) { } -} diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index d48d7ffcd824..c9b8712bdde9 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -228,7 +228,7 @@ open class ClockEventController @Inject constructor( listenForDozing(this) if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { listenForDozeAmountTransition(this) - listenForGoneToAodTransition(this) + listenForAnyStateToAodTransition(this) } else { listenForDozeAmount(this) } @@ -286,10 +286,10 @@ open class ClockEventController @Inject constructor( * dozing. */ @VisibleForTesting - internal fun listenForGoneToAodTransition(scope: CoroutineScope): Job { + internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job { return scope.launch { - keyguardTransitionInteractor.goneToAodTransition.filter { - it.transitionState == TransitionState.STARTED + keyguardTransitionInteractor.anyStateToAodTransition.filter { + it.transitionState == TransitionState.FINISHED }.collect { dozeAmount = 1f clock?.animations?.doze(dozeAmount) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt index 71470e8870de..a0206f1f1e70 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt @@ -35,6 +35,7 @@ data class KeyguardFingerprintListenModel( val keyguardOccluded: Boolean, val occludingAppRequestingFp: Boolean, val primaryUser: Boolean, + val shouldListenSfpsState: Boolean, val shouldListenForFingerprintAssistant: Boolean, val switchingUser: Boolean, val udfps: Boolean, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index c756a17976bf..2bb3a5f437f5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -106,8 +106,9 @@ public class KeyguardSecurityContainer extends ConstraintLayout { static final int USER_TYPE_WORK_PROFILE = 2; static final int USER_TYPE_SECONDARY_USER = 3; - @IntDef({MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER}) + @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER}) public @interface Mode {} + static final int MODE_UNINITIALIZED = -1; static final int MODE_DEFAULT = 0; static final int MODE_ONE_HANDED = 1; static final int MODE_USER_SWITCHER = 2; @@ -154,7 +155,11 @@ public class KeyguardSecurityContainer extends ConstraintLayout { private boolean mDisappearAnimRunning; private SwipeListener mSwipeListener; private ViewMode mViewMode = new DefaultViewMode(); - private @Mode int mCurrentMode = MODE_DEFAULT; + /* + * Using MODE_UNINITIALIZED to mean the view mode is set to DefaultViewMode, but init() has not + * yet been called on it. This will happen when the ViewController is initialized. + */ + private @Mode int mCurrentMode = MODE_UNINITIALIZED; private int mWidth = -1; private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback = @@ -347,6 +352,8 @@ public class KeyguardSecurityContainer extends ConstraintLayout { private String modeToString(@Mode int mode) { switch (mode) { + case MODE_UNINITIALIZED: + return "Uninitialized"; case MODE_DEFAULT: return "Default"; case MODE_ONE_HANDED: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 79a01b9c9717..fbb114c72add 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -318,6 +318,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard @Override public void onInit() { mSecurityViewFlipperController.init(); + configureMode(); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index bad75e8dd862..558869c46373 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -151,6 +151,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; +import com.android.systemui.util.settings.SecureSettings; import com.google.android.collect.Lists; @@ -337,17 +338,20 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>> mCallbacks = Lists.newArrayList(); private ContentObserver mDeviceProvisionedObserver; + private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver; private final ContentObserver mTimeFormatChangeObserver; private boolean mSwitchingUser; private boolean mDeviceInteractive; + private boolean mSfpsRequireScreenOnToAuthPrefEnabled; private final SubscriptionManager mSubscriptionManager; private final TelephonyListenerManager mTelephonyListenerManager; private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; private final BroadcastDispatcher mBroadcastDispatcher; + private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; private final StatusBarStateController mStatusBarStateController; @@ -396,6 +400,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab protected Handler getHandler() { return mHandler; } + private final Handler mHandler; private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = @@ -720,6 +725,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Request to listen for face authentication when an app is occluding keyguard. + * * @param request if true and mKeyguardOccluded, request face auth listening, else default * to normal behavior. * See {@link KeyguardUpdateMonitor#shouldListenForFace()} @@ -732,6 +738,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab /** * Request to listen for fingerprint when an app is occluding keyguard. + * * @param request if true and mKeyguardOccluded, request fingerprint listening, else default * to normal behavior. * See {@link KeyguardUpdateMonitor#shouldListenForFingerprint(boolean)} @@ -1946,6 +1953,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Context context, @Main Looper mainLooper, BroadcastDispatcher broadcastDispatcher, + SecureSettings secureSettings, DumpManager dumpManager, @Background Executor backgroundExecutor, @Main Executor mainExecutor, @@ -1988,6 +1996,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mStatusBarState = mStatusBarStateController.getState(); mLockPatternUtils = lockPatternUtils; mAuthController = authController; + mSecureSettings = secureSettings; dumpManager.registerDumpable(getClass().getName(), this); mSensorPrivacyManager = sensorPrivacyManager; mActiveUnlockConfig = activeUnlockConfiguration; @@ -2229,9 +2238,35 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Settings.System.TIME_12_24))); } }; + mContext.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.TIME_12_24), false, mTimeFormatChangeObserver, UserHandle.USER_ALL); + + updateSfpsRequireScreenOnToAuthPref(); + mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + updateSfpsRequireScreenOnToAuthPref(); + } + }; + + mContext.getContentResolver().registerContentObserver( + mSecureSettings.getUriFor( + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED), + false, + mSfpsRequireScreenOnToAuthPrefObserver, + getCurrentUser()); + } + + protected void updateSfpsRequireScreenOnToAuthPref() { + final int defaultSfpsRequireScreenOnToAuthValue = + mContext.getResources().getBoolean( + com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0; + mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser( + Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, + defaultSfpsRequireScreenOnToAuthValue, + getCurrentUser()) != 0; } private void initializeSimState() { @@ -2276,6 +2311,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if there's at least one sfps enrollment for the current user. + */ + public boolean isSfpsEnrolled() { + return mAuthController.isSfpsEnrolled(getCurrentUser()); + } + + /** + * @return true if sfps HW is supported on this device. Can return true even if the user has + * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered. + */ + public boolean isSfpsSupported() { + return mAuthController.getSfpsProps() != null + && !mAuthController.getSfpsProps().isEmpty(); + } + + /** * @return true if there's at least one face enrolled */ public boolean isFaceEnrolled() { @@ -2598,13 +2649,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab !(mFingerprintLockedOut && mBouncerIsOrWillBeShowing && mCredentialAttempted); final boolean isEncryptedOrLockdownForUser = isEncryptedOrLockdown(user); + final boolean shouldListenUdfpsState = !isUdfps || (!userCanSkipBouncer - && !isEncryptedOrLockdownForUser - && userDoesNotHaveTrust); + && !isEncryptedOrLockdownForUser + && userDoesNotHaveTrust); + + boolean shouldListenSideFpsState = true; + if (isSfpsSupported() && isSfpsEnrolled()) { + shouldListenSideFpsState = + mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true; + } - final boolean shouldListen = shouldListenKeyguardState && shouldListenUserState - && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut(); + boolean shouldListen = shouldListenKeyguardState && shouldListenUserState + && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut() + && shouldListenSideFpsState; maybeLogListenerModelData( new KeyguardFingerprintListenModel( @@ -2626,6 +2685,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardOccluded, mOccludingAppRequestingFp, mIsPrimaryUser, + shouldListenSideFpsState, shouldListenForFingerprintAssistant, mSwitchingUser, isUdfps, @@ -3727,6 +3787,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver); } + if (mSfpsRequireScreenOnToAuthPrefObserver != null) { + mContext.getContentResolver().unregisterContentObserver( + mSfpsRequireScreenOnToAuthPrefObserver); + } + try { ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver); } catch (RemoteException e) { @@ -3799,6 +3864,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab pw.println(" mBouncerIsOrWillBeShowing=" + mBouncerIsOrWillBeShowing); pw.println(" mStatusBarState=" + StatusBarState.toString(mStatusBarState)); pw.println(" mUdfpsBouncerShowing=" + mUdfpsBouncerShowing); + } else if (isSfpsSupported()) { + pw.println(" sfpsEnrolled=" + isSfpsEnrolled()); + pw.println(" shouldListenForSfps=" + shouldListenForFingerprint(false)); + pw.println(" mSfpsRequireScreenOnToAuthPrefEnabled=" + + mSfpsRequireScreenOnToAuthPrefEnabled); } } if (mFaceManager != null && mFaceManager.isHardwareDetected()) { diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index b888d54956cf..83747b4f8636 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -278,7 +278,11 @@ public class SystemUIApplication extends Application implements } private static void notifyBootCompleted(CoreStartable coreStartable) { - Trace.beginSection(coreStartable.getClass().getSimpleName() + ".onBootCompleted()"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, + coreStartable.getClass().getSimpleName() + ".onBootCompleted()"); + } coreStartable.onBootCompleted(); Trace.endSection(); } @@ -300,14 +304,18 @@ public class SystemUIApplication extends Application implements private static CoreStartable startAdditionalStartable(String clsName) { CoreStartable startable; if (DEBUG) Log.d(TAG, "loading: " + clsName); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, clsName + ".newInstance()"); + } try { - Trace.beginSection(clsName + ".newInstance()"); startable = (CoreStartable) Class.forName(clsName).newInstance(); - Trace.endSection(); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException ex) { throw new RuntimeException(ex); + } finally { + Trace.endSection(); } return startStartable(startable); @@ -315,7 +323,10 @@ public class SystemUIApplication extends Application implements private static CoreStartable startStartable(String clsName, Provider<CoreStartable> provider) { if (DEBUG) Log.d(TAG, "loading: " + clsName); - Trace.beginSection("Provider<" + clsName + ">.get()"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, "Provider<" + clsName + ">.get()"); + } CoreStartable startable = provider.get(); Trace.endSection(); return startStartable(startable); @@ -323,7 +334,10 @@ public class SystemUIApplication extends Application implements private static CoreStartable startStartable(CoreStartable startable) { if (DEBUG) Log.d(TAG, "running: " + startable); - Trace.beginSection(startable.getClass().getSimpleName() + ".start()"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, startable.getClass().getSimpleName() + ".start()"); + } startable.start(); Trace.endSection(); @@ -364,15 +378,22 @@ public class SystemUIApplication extends Application implements public void onConfigurationChanged(Configuration newConfig) { if (mServicesStarted) { ConfigurationController configController = mSysUIComponent.getConfigurationController(); - Trace.beginSection( - configController.getClass().getSimpleName() + ".onConfigurationChanged()"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, + configController.getClass().getSimpleName() + ".onConfigurationChanged()"); + } configController.onConfigurationChanged(newConfig); Trace.endSection(); int len = mServices.length; for (int i = 0; i < len; i++) { if (mServices[i] != null) { - Trace.beginSection( - mServices[i].getClass().getSimpleName() + ".onConfigurationChanged()"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, + mServices[i].getClass().getSimpleName() + + ".onConfigurationChanged()"); + } mServices[i].onConfigurationChanged(newConfig); Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index c015a21c7db4..ff18eeea45a5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -78,6 +78,7 @@ import com.android.systemui.doze.DozeReceiver; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.Execution; @@ -150,6 +151,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps; @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; + @NonNull private final SparseBooleanArray mSfpsEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; private boolean mAllFingerprintAuthenticatorsRegistered; @@ -159,6 +161,19 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final @Background DelayableExecutor mBackgroundExecutor; private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); + + private final VibratorHelper mVibratorHelper; + + private void vibrateSuccess(int modality) { + mVibratorHelper.vibrateAuthSuccess( + getClass().getSimpleName() + ", modality = " + modality + "BP::success"); + } + + private void vibrateError(int modality) { + mVibratorHelper.vibrateAuthError( + getClass().getSimpleName() + ", modality = " + modality + "BP::error"); + } + @VisibleForTesting final TaskStackListener mTaskStackListener = new TaskStackListener() { @Override @@ -325,6 +340,16 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } } + + if (mSidefpsProps == null) { + Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null"); + } else { + for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) { + if (prop.sensorId == sensorId) { + mSfpsEnrolledForUser.put(userId, hasEnrollments); + } + } + } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(); } @@ -660,7 +685,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull StatusBarStateController statusBarStateController, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, - @Background DelayableExecutor bgExecutor) { + @Background DelayableExecutor bgExecutor, + @NonNull VibratorHelper vibrator) { mContext = context; mExecution = execution; mUserManager = userManager; @@ -677,6 +703,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mWindowManager = windowManager; mInteractionJankMonitor = jankMonitor; mUdfpsEnrolledForUser = new SparseBooleanArray(); + mSfpsEnrolledForUser = new SparseBooleanArray(); + mVibratorHelper = vibrator; mOrientationListener = new BiometricDisplayListener( context, @@ -866,6 +894,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, public void onBiometricAuthenticated(@Modality int modality) { if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: "); + vibrateSuccess(modality); + if (mCurrentDialog != null) { mCurrentDialog.onAuthenticationSucceeded(modality); } else { @@ -889,6 +919,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsProps; } + @Nullable + public List<FingerprintSensorPropertiesInternal> getSfpsProps() { + return mSidefpsProps; + } + private String getErrorString(@Modality int modality, int error, int vendorCode) { switch (modality) { case TYPE_FACE: @@ -913,6 +948,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode)); } + vibrateError(modality); + final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT) || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT); @@ -1013,6 +1050,17 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsEnrolledForUser.get(userId); } + /** + * Whether the passed userId has enrolled SFPS. + */ + public boolean isSfpsEnrolled(int userId) { + if (mSidefpsController == null) { + return false; + } + + return mSfpsEnrolledForUser.get(userId); + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt index 5850c9537ef0..08c7c0fd0b03 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt @@ -127,7 +127,10 @@ open class UserBroadcastDispatcher( action, userId, { - Trace.beginSection("registerReceiver act=$action user=$userId") + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, "registerReceiver act=$action user=$userId") + } context.registerReceiverAsUser( this, UserHandle.of(userId), @@ -141,7 +144,11 @@ open class UserBroadcastDispatcher( }, { try { - Trace.beginSection("unregisterReceiver act=$action user=$userId") + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, + "unregisterReceiver act=$action user=$userId") + } context.unregisterReceiver(this) Trace.endSection() logger.logContextReceiverUnregistered(userId, action) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 6db562107357..e46bdb7a8ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -47,6 +47,7 @@ import com.android.systemui.keyguard.data.BouncerViewModule; import com.android.systemui.log.dagger.LogModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; +import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.navigationbar.NavigationBarComponent; import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; @@ -134,6 +135,7 @@ import dagger.Provides; FooterActionsModule.class, LogModule.class, MediaProjectionModule.class, + MotionToolModule.class, PeopleHubModule.class, PeopleModule.class, PluginModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index 60227ee95fc2..937884c79072 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -171,7 +171,10 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi @Override public void onSensorChanged(SensorEvent event) { - Trace.beginSection("DozeScreenBrightness.onSensorChanged" + event.values[0]); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, "DozeScreenBrightness.onSensorChanged" + event.values[0]); + } try { if (mRegistered) { mLastSensorValue = (int) event.values[0]; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt new file mode 100644 index 000000000000..d8dd6a21d4c1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.view.View +import androidx.core.animation.doOnEnd +import com.android.systemui.animation.Interpolators +import com.android.systemui.dreams.complication.ComplicationHostViewController +import com.android.systemui.dreams.complication.ComplicationLayoutParams +import com.android.systemui.dreams.dagger.DreamOverlayModule +import com.android.systemui.statusbar.BlurUtils +import java.util.function.Consumer +import javax.inject.Inject +import javax.inject.Named + +/** Controller for dream overlay animations. */ +class DreamOverlayAnimationsController +@Inject +constructor( + private val mBlurUtils: BlurUtils, + private val mComplicationHostViewController: ComplicationHostViewController, + private val mStatusBarViewController: DreamOverlayStatusBarViewController, + private val mOverlayStateController: DreamOverlayStateController, + @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) + private val mDreamInBlurAnimDuration: Int, + @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY) private val mDreamInBlurAnimDelay: Int, + @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) + private val mDreamInComplicationsAnimDuration: Int, + @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY) + private val mDreamInTopComplicationsAnimDelay: Int, + @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY) + private val mDreamInBottomComplicationsAnimDelay: Int +) { + + var mEntryAnimations: AnimatorSet? = null + + /** Starts the dream content and dream overlay entry animations. */ + fun startEntryAnimations(view: View) { + cancelRunningEntryAnimations() + + mEntryAnimations = AnimatorSet() + mEntryAnimations?.apply { + playTogether( + buildDreamInBlurAnimator(view), + buildDreamInTopComplicationsAnimator(), + buildDreamInBottomComplicationsAnimator() + ) + doOnEnd { mOverlayStateController.setEntryAnimationsFinished(true) } + start() + } + } + + /** Cancels the dream content and dream overlay animations, if they're currently running. */ + fun cancelRunningEntryAnimations() { + if (mEntryAnimations?.isRunning == true) { + mEntryAnimations?.cancel() + } + mEntryAnimations = null + } + + private fun buildDreamInBlurAnimator(view: View): Animator { + return ValueAnimator.ofFloat(1f, 0f).apply { + duration = mDreamInBlurAnimDuration.toLong() + startDelay = mDreamInBlurAnimDelay.toLong() + interpolator = Interpolators.LINEAR + addUpdateListener { animator: ValueAnimator -> + mBlurUtils.applyBlur( + view.viewRootImpl, + mBlurUtils.blurRadiusOfRatio(animator.animatedValue as Float).toInt(), + false /*opaque*/ + ) + } + } + } + + private fun buildDreamInTopComplicationsAnimator(): Animator { + return ValueAnimator.ofFloat(0f, 1f).apply { + duration = mDreamInComplicationsAnimDuration.toLong() + startDelay = mDreamInTopComplicationsAnimDelay.toLong() + interpolator = Interpolators.LINEAR + addUpdateListener { va: ValueAnimator -> + setTopElementsAlpha(va.animatedValue as Float) + } + } + } + + private fun buildDreamInBottomComplicationsAnimator(): Animator { + return ValueAnimator.ofFloat(0f, 1f).apply { + duration = mDreamInComplicationsAnimDuration.toLong() + startDelay = mDreamInBottomComplicationsAnimDelay.toLong() + interpolator = Interpolators.LINEAR + addUpdateListener { va: ValueAnimator -> + setBottomElementsAlpha(va.animatedValue as Float) + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + mComplicationHostViewController + .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM) + .forEach(Consumer { v: View -> v.visibility = View.VISIBLE }) + } + } + ) + } + } + + /** Sets alpha of top complications and the status bar. */ + private fun setTopElementsAlpha(alpha: Float) { + mComplicationHostViewController + .getViewsAtPosition(ComplicationLayoutParams.POSITION_TOP) + .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) }) + mStatusBarViewController.setAlpha(alpha) + } + + /** Sets alpha of bottom complications. */ + private fun setBottomElementsAlpha(alpha: Float) { + mComplicationHostViewController + .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM) + .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) }) + } + + private fun setAlphaAndEnsureVisible(view: View, alpha: Float) { + if (alpha > 0 && view.visibility != View.VISIBLE) { + view.visibility = View.VISIBLE + } + + view.alpha = alpha + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 733a80dd7f69..d0d01846c3d9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -54,6 +54,8 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve private final DreamOverlayStatusBarViewController mStatusBarViewController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final BlurUtils mBlurUtils; + private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; + private final DreamOverlayStateController mStateController; private final ComplicationHostViewController mComplicationHostViewController; @@ -134,12 +136,16 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long burnInProtectionUpdateInterval, @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter, - BouncerCallbackInteractor bouncerCallbackInteractor) { + BouncerCallbackInteractor bouncerCallbackInteractor, + DreamOverlayAnimationsController animationsController, + DreamOverlayStateController stateController) { super(containerView); mDreamOverlayContentView = contentView; mStatusBarViewController = statusBarViewController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mBlurUtils = blurUtils; + mDreamOverlayAnimationsController = animationsController; + mStateController = stateController; mComplicationHostViewController = complicationHostViewController; mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize( @@ -172,6 +178,11 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback); } mBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback); + + // Start dream entry animations. Skip animations for low light clock. + if (!mStateController.isLowLightActive()) { + mDreamOverlayAnimationsController.startEntryAnimations(mView); + } } @Override @@ -182,6 +193,8 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback); } mBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback); + + mDreamOverlayAnimationsController.cancelRunningEntryAnimations(); } View getContainerView() { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index d1b73685a3f3..8542412f82f8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -90,13 +90,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ new KeyguardUpdateMonitorCallback() { @Override public void onShadeExpandedChanged(boolean expanded) { - if (mLifecycleRegistry.getCurrentState() != Lifecycle.State.RESUMED - && mLifecycleRegistry.getCurrentState() != Lifecycle.State.STARTED) { - return; - } - - mLifecycleRegistry.setCurrentState( - expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + mExecutor.execute(() -> { + if (getCurrentStateLocked() != Lifecycle.State.RESUMED + && getCurrentStateLocked() != Lifecycle.State.STARTED) { + return; + } + + setCurrentStateLocked( + expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + }); } }; @@ -146,29 +148,30 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ () -> mExecutor.execute(DreamOverlayService.this::requestExit); mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host); mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry(); - setCurrentState(Lifecycle.State.CREATED); - } - private void setCurrentState(Lifecycle.State state) { - mExecutor.execute(() -> mLifecycleRegistry.setCurrentState(state)); + mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED)); } @Override public void onDestroy() { mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); - setCurrentState(Lifecycle.State.DESTROYED); - resetCurrentDreamOverlay(); + mExecutor.execute(() -> { + setCurrentStateLocked(Lifecycle.State.DESTROYED); + + resetCurrentDreamOverlayLocked(); + + mDestroyed = true; + }); - mDestroyed = true; super.onDestroy(); } @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { - setCurrentState(Lifecycle.State.STARTED); - mExecutor.execute(() -> { + setCurrentStateLocked(Lifecycle.State.STARTED); + mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); if (mDestroyed) { @@ -181,7 +184,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // Reset the current dream overlay before starting a new one. This can happen // when two dreams overlap (briefly, for a smoother dream transition) and both // dreams are bound to the dream overlay service. - resetCurrentDreamOverlay(); + resetCurrentDreamOverlayLocked(); } mDreamOverlayContainerViewController = @@ -191,7 +194,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStateController.setShouldShowComplications(shouldShowComplications()); addOverlayWindowLocked(layoutParams); - setCurrentState(Lifecycle.State.RESUMED); + setCurrentStateLocked(Lifecycle.State.RESUMED); mStateController.setOverlayActive(true); final ComponentName dreamComponent = getDreamComponent(); mStateController.setLowLightActive( @@ -202,6 +205,14 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ }); } + private Lifecycle.State getCurrentStateLocked() { + return mLifecycleRegistry.getCurrentState(); + } + + private void setCurrentStateLocked(Lifecycle.State state) { + mLifecycleRegistry.setCurrentState(state); + } + /** * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be * called from the main executing thread. The window attributes closely mirror those that are @@ -231,13 +242,13 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // Make extra sure the container view has been removed from its old parent (otherwise we // risk an IllegalStateException in some cases when setting the container view as the // window's content view and the container view hasn't been properly removed previously). - removeContainerViewFromParent(); + removeContainerViewFromParentLocked(); mWindow.setContentView(mDreamOverlayContainerViewController.getContainerView()); mWindowManager.addView(mWindow.getDecorView(), mWindow.getAttributes()); } - private void removeContainerViewFromParent() { + private void removeContainerViewFromParentLocked() { View containerView = mDreamOverlayContainerViewController.getContainerView(); if (containerView == null) { return; @@ -250,13 +261,14 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ parentView.removeView(containerView); } - private void resetCurrentDreamOverlay() { + private void resetCurrentDreamOverlayLocked() { if (mStarted && mWindow != null) { mWindowManager.removeView(mWindow.getDecorView()); } mStateController.setOverlayActive(false); mStateController.setLowLightActive(false); + mStateController.setEntryAnimationsFinished(false); mDreamOverlayContainerViewController = null; mDreamOverlayTouchMonitor = null; diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index 72feaca59ace..e80d0beabf2b 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -51,6 +51,7 @@ public class DreamOverlayStateController implements public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0; public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1; + public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2; private static final int OP_CLEAR_STATE = 1; private static final int OP_SET_STATE = 2; @@ -202,6 +203,14 @@ public class DreamOverlayStateController implements return containsState(STATE_LOW_LIGHT_ACTIVE); } + /** + * Returns whether the dream content and dream overlay entry animations are finished. + * @return {@code true} if animations are finished, {@code false} otherwise. + */ + public boolean areEntryAnimationsFinished() { + return containsState(STATE_DREAM_ENTRY_ANIMATIONS_FINISHED); + } + private boolean containsState(int state) { return (mState & state) != 0; } @@ -218,7 +227,7 @@ public class DreamOverlayStateController implements } if (existingState != mState) { - notifyCallbacks(callback -> callback.onStateChanged()); + notifyCallbacks(Callback::onStateChanged); } } @@ -239,6 +248,15 @@ public class DreamOverlayStateController implements } /** + * Sets whether dream content and dream overlay entry animations are finished. + * @param finished {@code true} if entry animations are finished, {@code false} otherwise. + */ + public void setEntryAnimationsFinished(boolean finished) { + modifyState(finished ? OP_SET_STATE : OP_CLEAR_STATE, + STATE_DREAM_ENTRY_ANIMATIONS_FINISHED); + } + + /** * Returns the available complication types. */ @Complication.ComplicationType diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java index bb1c4303041a..d17fbe31c8d2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java @@ -16,10 +16,6 @@ package com.android.systemui.dreams; -import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; -import static android.app.StatusBarManager.WINDOW_STATE_HIDING; -import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; - import android.app.AlarmManager; import android.app.StatusBarManager; import android.content.res.Resources; @@ -83,6 +79,9 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve private boolean mIsAttached; + // Whether dream entry animations are finished. + private boolean mEntryAnimationsFinished = false; + private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder() .clearCapabilities() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); @@ -109,7 +108,9 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve new DreamOverlayStateController.Callback() { @Override public void onStateChanged() { - updateLowLightState(); + mEntryAnimationsFinished = + mDreamOverlayStateController.areEntryAnimationsFinished(); + updateVisibility(); } }; @@ -195,7 +196,6 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback); mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback); - updateLowLightState(); mTouchInsetSession.addViewToTracking(mView); } @@ -216,6 +216,26 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve mIsAttached = false; } + /** + * Sets alpha of the dream overlay status bar. + * + * No-op if the dream overlay status bar should not be shown. + */ + protected void setAlpha(float alpha) { + updateVisibility(); + + if (mView.getVisibility() != View.VISIBLE) { + return; + } + + mView.setAlpha(alpha); + } + + private boolean shouldShowStatusBar() { + return !mDreamOverlayStateController.isLowLightActive() + && !mStatusBarWindowStateController.windowIsShowing(); + } + private void updateWifiUnavailableStatusIcon() { final NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities( @@ -235,13 +255,12 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve hasAlarm ? buildAlarmContentDescription(alarm) : null); } - private void updateLowLightState() { - int visibility = View.VISIBLE; - if (mDreamOverlayStateController.isLowLightActive() - || mStatusBarWindowStateController.windowIsShowing()) { - visibility = View.INVISIBLE; + private void updateVisibility() { + if (shouldShowStatusBar()) { + mView.setVisibility(View.VISIBLE); + } else { + mView.setVisibility(View.INVISIBLE); } - mView.setVisibility(visibility); } private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) { @@ -298,21 +317,11 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve } private void onSystemStatusBarStateChanged(@StatusBarManager.WindowVisibleState int state) { - mMainExecutor.execute(() -> { - if (!mIsAttached || mDreamOverlayStateController.isLowLightActive()) { - return; - } + if (!mIsAttached || !mEntryAnimationsFinished) { + return; + } - switch (state) { - case WINDOW_STATE_SHOWING: - mView.setVisibility(View.INVISIBLE); - break; - case WINDOW_STATE_HIDING: - case WINDOW_STATE_HIDDEN: - mView.setVisibility(View.VISIBLE); - break; - } - }); + mMainExecutor.execute(this::updateVisibility); } private void onStatusBarItemsChanged(List<StatusBarItem> newItems) { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java index fd6cfc0700ad..100ccc35e638 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java @@ -28,6 +28,7 @@ import android.view.View; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.lifecycle.LifecycleOwner; +import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.util.ViewController; import java.util.Collection; @@ -49,20 +50,34 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final ComplicationLayoutEngine mLayoutEngine; + private final DreamOverlayStateController mDreamOverlayStateController; private final LifecycleOwner mLifecycleOwner; private final ComplicationCollectionViewModel mComplicationCollectionViewModel; private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>(); + // Whether dream entry animations are finished. + private boolean mEntryAnimationsFinished = false; + @Inject protected ComplicationHostViewController( @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view, ComplicationLayoutEngine layoutEngine, + DreamOverlayStateController dreamOverlayStateController, LifecycleOwner lifecycleOwner, @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) { super(view); mLayoutEngine = layoutEngine; mLifecycleOwner = lifecycleOwner; mComplicationCollectionViewModel = viewModel; + mDreamOverlayStateController = dreamOverlayStateController; + + mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() { + @Override + public void onStateChanged() { + mEntryAnimationsFinished = + mDreamOverlayStateController.areEntryAnimationsFinished(); + } + }); } @Override @@ -123,6 +138,11 @@ public class ComplicationHostViewController extends ViewController<ConstraintLay final ComplicationId id = complication.getId(); final Complication.ViewHolder viewHolder = complication.getComplication() .createView(complication); + // Complications to be added before dream entry animations are finished are set + // to invisible and are animated in. + if (!mEntryAnimationsFinished) { + viewHolder.getView().setVisibility(View.INVISIBLE); + } mComplications.put(id, viewHolder); if (viewHolder.getView().getParent() != null) { Log.e(TAG, "View for complication " diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java index 4fe1622d73a5..cb012fa42e94 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java @@ -47,6 +47,14 @@ public abstract class DreamOverlayModule { public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL = "burn_in_protection_update_interval"; public static final String MILLIS_UNTIL_FULL_JITTER = "millis_until_full_jitter"; + public static final String DREAM_IN_BLUR_ANIMATION_DURATION = "dream_in_blur_anim_duration"; + public static final String DREAM_IN_BLUR_ANIMATION_DELAY = "dream_in_blur_anim_delay"; + public static final String DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = + "dream_in_complications_anim_duration"; + public static final String DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = + "dream_in_top_complications_anim_delay"; + public static final String DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = + "dream_in_bottom_complications_anim_delay"; /** */ @Provides @@ -114,6 +122,51 @@ public abstract class DreamOverlayModule { return resources.getInteger(R.integer.config_dreamOverlayMillisUntilFullJitter); } + /** + * Duration in milliseconds of the dream in un-blur animation. + */ + @Provides + @Named(DREAM_IN_BLUR_ANIMATION_DURATION) + static int providesDreamInBlurAnimationDuration(@Main Resources resources) { + return resources.getInteger(R.integer.config_dreamOverlayInBlurDurationMs); + } + + /** + * Delay in milliseconds of the dream in un-blur animation. + */ + @Provides + @Named(DREAM_IN_BLUR_ANIMATION_DELAY) + static int providesDreamInBlurAnimationDelay(@Main Resources resources) { + return resources.getInteger(R.integer.config_dreamOverlayInBlurDelayMs); + } + + /** + * Duration in milliseconds of the dream in complications fade-in animation. + */ + @Provides + @Named(DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) + static int providesDreamInComplicationsAnimationDuration(@Main Resources resources) { + return resources.getInteger(R.integer.config_dreamOverlayInComplicationsDurationMs); + } + + /** + * Delay in milliseconds of the dream in top complications fade-in animation. + */ + @Provides + @Named(DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY) + static int providesDreamInTopComplicationsAnimationDelay(@Main Resources resources) { + return resources.getInteger(R.integer.config_dreamOverlayInTopComplicationsDelayMs); + } + + /** + * Delay in milliseconds of the dream in bottom complications fade-in animation. + */ + @Provides + @Named(DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY) + static int providesDreamInBottomComplicationsAnimationDelay(@Main Resources resources) { + return resources.getInteger(R.integer.config_dreamOverlayInBottomComplicationsDelayMs); + } + @Provides @DreamOverlayComponent.DreamOverlayScope static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 029ffae267d1..974f6058a1a3 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -57,7 +57,7 @@ object Flags { val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true) // TODO(b/254512425): Tracking Bug - val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = true) + val NOTIFICATION_MEMORY_MONITOR_ENABLED = ReleasedFlag(112) // TODO(b/254512731): Tracking Bug @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true) @@ -125,6 +125,14 @@ object Flags { // TODO(b/255607168): Tracking Bug @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213) + /** + * Whether to enable the code powering customizable lock screen quick affordances. + * + * Note that this flag does not enable individual implementations of quick affordances like the + * new camera quick affordance. Look for individual flags for those. + */ + @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = UnreleasedFlag(214, teamfood = false) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = ReleasedFlag(300) @@ -171,10 +179,18 @@ object Flags { @Deprecated("Replaced by mobile and wifi specific flags.") val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false) + // TODO(b/256614753): Tracking Bug val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606) + // TODO(b/256614210): Tracking Bug val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607) + // TODO(b/256614751): Tracking Bug + val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = UnreleasedFlag(608) + + // TODO(b/256613548): Tracking Bug + val NEW_STATUS_BAR_WIFI_ICON_BACKEND = UnreleasedFlag(609) + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700) @@ -336,6 +352,9 @@ object Flags { // 1900 - note task @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks") + // 2000 - device controls + @Keep val USE_APP_PANELS = UnreleasedFlag(2000, true) + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java index 546a4093ec7d..450fa1420408 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java @@ -33,6 +33,8 @@ import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.widget.ImageView; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; @@ -61,6 +63,7 @@ public class WorkLockActivity extends Activity { private UserManager mUserManager; private PackageManager mPackageManager; private final BroadcastDispatcher mBroadcastDispatcher; + private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; @Inject public WorkLockActivity(BroadcastDispatcher broadcastDispatcher, UserManager userManager, @@ -95,6 +98,10 @@ public class WorkLockActivity extends Activity { if (badgedIcon != null) { ((ImageView) findViewById(R.id.icon)).setImageDrawable(badgedIcon); } + + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + mBackCallback); } @VisibleForTesting @@ -134,11 +141,16 @@ public class WorkLockActivity extends Activity { @Override public void onDestroy() { unregisterBroadcastReceiver(); + getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback); super.onDestroy(); } @Override public void onBackPressed() { + onBackInvoked(); + } + + private void onBackInvoked() { // Ignore back presses. } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56a1f1ae936e..f08463bfd5e9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -42,6 +42,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; @@ -71,6 +72,7 @@ import dagger.Provides; KeyguardUserSwitcherComponent.class}, includes = { FalsingModule.class, + KeyguardDataQuickAffordanceModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, StartKeyguardTransitionModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index c600e13cc2dd..d6f521c6dc87 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -53,6 +53,10 @@ constructor( override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + override val pickerName: String by lazy { context.getString(component.getTileTitleId()) } + + override val pickerIconResourceId: Int by lazy { component.getTileImageId() } + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked -> if (canShowWhileLocked) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt new file mode 100644 index 000000000000..bea9363efc81 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import dagger.Module +import dagger.Provides +import dagger.multibindings.ElementsIntoSet + +@Module +object KeyguardDataQuickAffordanceModule { + @Provides + @ElementsIntoSet + fun quickAffordanceConfigs( + home: HomeControlsKeyguardQuickAffordanceConfig, + quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig, + qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig, + ): Set<KeyguardQuickAffordanceConfig> { + return setOf( + home, + quickAccessWallet, + qrCodeScanner, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 0a8090bb11ac..fd40d1dd1a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -29,6 +29,10 @@ interface KeyguardQuickAffordanceConfig { /** Unique identifier for this quick affordance. It must be globally unique. */ val key: String + val pickerName: String + + val pickerIconResourceId: Int + /** * The ever-changing state of the affordance. * diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt new file mode 100644 index 000000000000..9c9354fec695 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Manages and provides access to the current "selections" of keyguard quick affordances, answering + * the question "which affordances should the keyguard show?". + */ +@SysUISingleton +class KeyguardQuickAffordanceSelectionManager @Inject constructor() { + + // TODO(b/254858695): implement a persistence layer (database). + private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + + /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ + val selections: Flow<Map<String, List<String>>> = _selections.asStateFlow() + + /** + * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in + * descending priority order. + */ + suspend fun getSelections(): Map<String, List<String>> { + return _selections.value + } + + /** + * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance + * IDs should be descending priority order. + */ + suspend fun setSelections( + slotId: String, + affordanceIds: List<String>, + ) { + // Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit + // when we set its value to the same instance of the original map, even if we change the + // map by updating the value of one of its keys. + val copy = _selections.value.toMutableMap() + copy[slotId] = affordanceIds + _selections.value = copy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index d620b2a1654c..11f72ffa4757 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Context import com.android.systemui.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging @@ -24,6 +25,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -34,11 +36,16 @@ import kotlinx.coroutines.flow.Flow class QrCodeScannerKeyguardQuickAffordanceConfig @Inject constructor( + @Application context: Context, private val controller: QRCodeScannerController, ) : KeyguardQuickAffordanceConfig { override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + override val pickerName = context.getString(R.string.qr_code_scanner_title) + + override val pickerIconResourceId = R.drawable.ic_qr_code_scanner + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index be57a323b5d1..303e6a1a95cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Context import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsError import android.service.quickaccesswallet.GetWalletCardsResponse @@ -29,6 +30,7 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.wallet.controller.QuickAccessWalletController import javax.inject.Inject @@ -40,12 +42,17 @@ import kotlinx.coroutines.flow.Flow class QuickAccessWalletKeyguardQuickAffordanceConfig @Inject constructor( + @Application context: Context, private val walletController: QuickAccessWalletController, private val activityStarter: ActivityStarter, ) : KeyguardQuickAffordanceConfig { override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + override val pickerName = context.getString(R.string.accessibility_wallet_button) + + override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen + override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt new file mode 100644 index 000000000000..95f614fbf7b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation +import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** Abstracts access to application state related to keyguard quick affordances. */ +@SysUISingleton +class KeyguardQuickAffordanceRepository +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val selectionManager: KeyguardQuickAffordanceSelectionManager, + private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, +) { + /** + * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the + * given ID. The configs are sorted in descending priority order. + */ + val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> = + selectionManager.selections + .map { selectionsBySlotId -> + selectionsBySlotId.mapValues { (_, selections) -> + configs.filter { selections.contains(it.key) } + } + } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyMap(), + ) + + /** + * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the + * slot with the given ID. The configs are sorted in descending priority order. + */ + suspend fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> { + val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList()) + return configs.filter { selections.contains(it.key) } + } + + /** + * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs + * are sorted in descending priority order. + */ + suspend fun getSelections(): Map<String, List<String>> { + return selectionManager.getSelections() + } + + /** + * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance + * IDs should be descending priority order. + */ + fun setSelections( + slotId: String, + affordanceIds: List<String>, + ) { + scope.launch(backgroundDispatcher) { + selectionManager.setSelections( + slotId = slotId, + affordanceIds = affordanceIds, + ) + } + } + + /** + * Returns the list of representation objects for all known affordances, regardless of what is + * selected. This is useful for building experiences like the picker/selector or user settings + * so the user can see everything that can be selected in a menu. + */ + fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> { + return configs.map { config -> + KeyguardQuickAffordancePickerRepresentation( + id = config.key, + name = config.pickerName, + iconResourceId = config.pickerIconResourceId, + ) + } + } + + /** + * Returns the list of representation objects for all available slots on the keyguard. This is + * useful for building experiences like the picker/selector or user settings so the user can see + * each slot and select which affordance(s) is/are installed in each slot on the keyguard. + */ + fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { + // TODO(b/256195304): source these from a config XML file. + return listOf( + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + ), + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + ), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 6baaf5f10024..c867c6e9229b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -23,9 +23,12 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.doze.DozeHost import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -65,6 +68,9 @@ interface KeyguardRepository { */ val isKeyguardShowing: Flow<Boolean> + /** Observable for whether the bouncer is showing. */ + val isBouncerShowing: Flow<Boolean> + /** * Observable for whether we are in doze state. * @@ -95,6 +101,9 @@ interface KeyguardRepository { /** Observable for device wake/sleep state */ val wakefulnessState: Flow<WakefulnessModel> + /** Observable for biometric unlock modes */ + val biometricUnlockState: Flow<BiometricUnlockModel> + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -125,6 +134,7 @@ constructor( private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, wakefulnessLifecycle: WakefulnessLifecycle, + biometricUnlockController: BiometricUnlockController, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -159,6 +169,29 @@ constructor( awaitClose { keyguardStateController.removeCallback(callback) } } + override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onBouncerShowingChanged() { + trySendWithFailureLogging( + keyguardStateController.isBouncerShowing, + TAG, + "updated isBouncerShowing" + ) + } + } + + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isBouncerShowing, + TAG, + "initial isBouncerShowing" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } + } + override val isDozing: Flow<Boolean> = conflatedCallbackFlow { val callback = @@ -248,6 +281,24 @@ constructor( awaitClose { wakefulnessLifecycle.removeObserver(callback) } } + override val biometricUnlockState: Flow<BiometricUnlockModel> = conflatedCallbackFlow { + val callback = + object : BiometricUnlockController.BiometricModeListener { + override fun onModeChanged(@WakeAndUnlockMode mode: Int) { + trySendWithFailureLogging(biometricModeIntToObject(mode), TAG, "biometric mode") + } + } + + biometricUnlockController.addBiometricModeListener(callback) + trySendWithFailureLogging( + biometricModeIntToObject(biometricUnlockController.getMode()), + TAG, + "initial biometric mode" + ) + + awaitClose { biometricUnlockController.removeBiometricModeListener(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -279,6 +330,20 @@ constructor( } } + private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockModel { + return when (value) { + 0 -> BiometricUnlockModel.NONE + 1 -> BiometricUnlockModel.WAKE_AND_UNLOCK + 2 -> BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING + 3 -> BiometricUnlockModel.SHOW_BOUNCER + 4 -> BiometricUnlockModel.ONLY_WAKE + 5 -> BiometricUnlockModel.UNLOCK_COLLAPSING + 6 -> BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM + 7 -> BiometricUnlockModel.DISMISS_BOUNCER + else -> throw IllegalArgumentException("Invalid BiometricUnlockModel value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt new file mode 100644 index 000000000000..7e01db34319f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodToGoneTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionRepository: KeyguardTransitionRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor("AOD->GONE") { + + private val wakeAndUnlockModes = + setOf(WAKE_AND_UNLOCK, WAKE_AND_UNLOCK_FROM_DREAM, WAKE_AND_UNLOCK_PULSING) + + override fun start() { + scope.launch { + keyguardInteractor.biometricUnlockState + .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (biometricUnlockState, keyguardState) = pair + if ( + keyguardState == KeyguardState.AOD && + wakeAndUnlockModes.contains(biometricUnlockState) + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 03c6a789326e..614ff8d930d8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -39,10 +41,19 @@ constructor( val dozeAmount: Flow<Float> = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing - /** Whether the keyguard is showing to not. */ + /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** Whether the bouncer is showing or not. */ + val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing /** The device wake/sleep state */ val wakefulnessState: Flow<WakefulnessModel> = repository.wakefulnessState + /** Observable for the [StatusBarState] */ + val statusBarState: Flow<StatusBarState> = repository.statusBarState + /** + * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, + * side, under display) is used to unlock the device. + */ + val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 13d97aaf28da..92caa89bb0e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -18,19 +18,30 @@ package com.android.systemui.keyguard.domain.interactor import android.content.Intent +import android.util.Log import com.android.internal.widget.LockPatternUtils import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation +import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @SysUISingleton @@ -43,7 +54,12 @@ constructor( private val keyguardStateController: KeyguardStateController, private val userTracker: UserTracker, private val activityStarter: ActivityStarter, + private val featureFlags: FeatureFlags, + private val repository: Lazy<KeyguardQuickAffordanceRepository>, ) { + private val isUsingRepository: Boolean + get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** Returns an observable for the quick affordance at the given position. */ fun quickAffordance( position: KeyguardQuickAffordancePosition @@ -72,7 +88,19 @@ constructor( configKey: String, expandable: Expandable?, ) { - @Suppress("UNCHECKED_CAST") val config = registry.get(configKey) + @Suppress("UNCHECKED_CAST") + val config = + if (isUsingRepository) { + val (slotId, decodedConfigKey) = configKey.decode() + repository.get().selections.value[slotId]?.find { it.key == decodedConfigKey } + } else { + registry.get(configKey) + } + if (config == null) { + Log.e(TAG, "Affordance config with key of \"$configKey\" not found!") + return + } + when (val result = config.onTriggered(expandable)) { is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> launchQuickAffordance( @@ -84,28 +112,138 @@ constructor( } } + /** + * Selects an affordance with the given ID on the slot with the given ID. + * + * @return `true` if the affordance was selected successfully; `false` otherwise. + */ + suspend fun select(slotId: String, affordanceId: String): Boolean { + check(isUsingRepository) + + val slots = repository.get().getSlotPickerRepresentations() + val slot = slots.find { it.id == slotId } ?: return false + val selections = + repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + val alreadySelected = selections.remove(affordanceId) + if (!alreadySelected) { + while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) { + selections.removeAt(0) + } + } + + selections.add(affordanceId) + + repository + .get() + .setSelections( + slotId = slotId, + affordanceIds = selections, + ) + + return true + } + + /** + * Unselects one or all affordances from the slot with the given ID. + * + * @param slotId The ID of the slot. + * @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances + * from the slot. + * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if + * the affordance was not on the slot to begin with). + */ + suspend fun unselect(slotId: String, affordanceId: String?): Boolean { + check(isUsingRepository) + + val slots = repository.get().getSlotPickerRepresentations() + if (slots.find { it.id == slotId } == null) { + return false + } + + if (affordanceId.isNullOrEmpty()) { + return if ( + repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty() + ) { + false + } else { + repository.get().setSelections(slotId = slotId, affordanceIds = emptyList()) + true + } + } + + val selections = + repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList() + return if (selections.remove(affordanceId)) { + repository + .get() + .setSelections( + slotId = slotId, + affordanceIds = selections, + ) + true + } else { + false + } + } + + /** Returns affordance IDs indexed by slot ID, for all known slots. */ + suspend fun getSelections(): Map<String, List<String>> { + check(isUsingRepository) + + val selections = repository.get().getSelections() + return repository.get().getSlotPickerRepresentations().associate { slotRepresentation -> + slotRepresentation.id to (selections[slotRepresentation.id] ?: emptyList()) + } + } + private fun quickAffordanceInternal( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceModel> { - val configs = registry.getAll(position) + return if (isUsingRepository) { + repository + .get() + .selections + .map { it[position.toSlotId()] ?: emptyList() } + .flatMapLatest { configs -> combinedConfigs(position, configs) } + } else { + combinedConfigs(position, registry.getAll(position)) + } + } + + private fun combinedConfigs( + position: KeyguardQuickAffordancePosition, + configs: List<KeyguardQuickAffordanceConfig>, + ): Flow<KeyguardQuickAffordanceModel> { + if (configs.isEmpty()) { + return flowOf(KeyguardQuickAffordanceModel.Hidden) + } + return combine( configs.map { config -> - // We emit an initial "Hidden" value to make sure that there's always an initial - // value and avoid subtle bugs where the downstream isn't receiving any values - // because one config implementation is not emitting an initial value. For example, - // see b/244296596. + // We emit an initial "Hidden" value to make sure that there's always an + // initial value and avoid subtle bugs where the downstream isn't receiving + // any values because one config implementation is not emitting an initial + // value. For example, see b/244296596. config.lockScreenState.onStart { emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) } } ) { states -> val index = - states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible } + states.indexOfFirst { state -> + state is KeyguardQuickAffordanceConfig.LockScreenState.Visible + } if (index != -1) { val visibleState = states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible + val configKey = configs[index].key KeyguardQuickAffordanceModel.Visible( - configKey = configs[index].key, + configKey = + if (isUsingRepository) { + configKey.encode(position.toSlotId()) + } else { + configKey + }, icon = visibleState.icon, activationState = visibleState.activationState, ) @@ -145,4 +283,39 @@ constructor( ) } } + + private fun KeyguardQuickAffordancePosition.toSlotId(): String { + return when (this) { + KeyguardQuickAffordancePosition.BOTTOM_START -> + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + KeyguardQuickAffordancePosition.BOTTOM_END -> + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + } + } + + private fun String.encode(slotId: String): String { + return "$slotId$DELIMITER$this" + } + + private fun String.decode(): Pair<String, String> { + val splitUp = this.split(DELIMITER) + return Pair(splitUp[0], splitUp[1]) + } + + fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> { + check(isUsingRepository) + + return repository.get().getAffordancePickerRepresentations() + } + + fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { + check(isUsingRepository) + + return repository.get().getSlotPickerRepresentations() + } + + companion object { + private const val TAG = "KeyguardQuickAffordanceInteractor" + private const val DELIMITER = "::" + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index 83d94325b9d4..57fb4a114700 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -41,11 +41,13 @@ constructor( } scope.launch { - interactor.finishedKeyguardState.collect { logger.i("Finished transition to", it) } + interactor.finishedKeyguardTransitionStep.collect { + logger.i("Finished transition", it) + } } scope.launch { - interactor.startedKeyguardState.collect { logger.i("Started transition to", it) } + interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index d5ea77b8e729..a7c6d4450336 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -41,6 +41,7 @@ constructor( is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it") is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it") + is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it") } it.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index dffd097a77c5..749183e241e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -21,7 +21,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD -import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -44,8 +43,9 @@ constructor( /** LOCKSCREEN->AOD transition information. */ val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) - /** GONE->AOD information. */ - val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD) + /** (any)->AOD transition information */ + val anyStateToAodTransition: Flow<TransitionStep> = + repository.transitions.filter { step -> step.to == KeyguardState.AOD } /** * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> @@ -57,15 +57,15 @@ constructor( lockscreenToAodTransition, ) + /* The last [TransitionStep] with a [TransitionState] of FINISHED */ + val finishedKeyguardTransitionStep: Flow<TransitionStep> = + repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } + /* The last completed [KeyguardState] transition */ val finishedKeyguardState: Flow<KeyguardState> = - repository.transitions - .filter { step -> step.transitionState == TransitionState.FINISHED } - .map { step -> step.to } + finishedKeyguardTransitionStep.map { step -> step.to } - /* The last started [KeyguardState] transition */ - val startedKeyguardState: Flow<KeyguardState> = - repository.transitions - .filter { step -> step.transitionState == TransitionState.STARTED } - .map { step -> step.to } + /* The last [TransitionStep] with a [TransitionState] of STARTED */ + val startedKeyguardTransitionStep: Flow<TransitionStep> = + repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt index 761f3fd9d9f9..fd4814d2bc94 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -16,14 +16,16 @@ package com.android.systemui.keyguard.domain.interactor +import android.animation.ValueAnimator +import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -38,7 +40,7 @@ class LockscreenBouncerTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, - private val keyguardRepository: KeyguardRepository, + private val keyguardInteractor: KeyguardInteractor, private val shadeRepository: ShadeRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor @@ -47,12 +49,47 @@ constructor( private var transitionId: UUID? = null override fun start() { + listenForDraggingUpToBouncer() + listenForBouncerHiding() + } + + private fun listenForBouncerHiding() { + scope.launch { + keyguardInteractor.isBouncerShowing + .sample(keyguardInteractor.wakefulnessState, { a, b -> Pair(a, b) }) + .collect { pair -> + val (isBouncerShowing, wakefulnessState) = pair + if (!isBouncerShowing) { + val to = + if ( + wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP || + wakefulnessState == WakefulnessModel.ASLEEP + ) { + KeyguardState.AOD + } else { + KeyguardState.LOCKSCREEN + } + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = to, + animator = getAnimator(), + ) + ) + } + } + } + } + + /* Starts transitions when manually dragging up the bouncer from the lockscreen. */ + private fun listenForDraggingUpToBouncer() { scope.launch { shadeRepository.shadeModel .sample( combine( keyguardTransitionInteractor.finishedKeyguardState, - keyguardRepository.statusBarState, + keyguardInteractor.statusBarState, ) { keyguardState, statusBarState -> Pair(keyguardState, statusBarState) }, @@ -100,4 +137,15 @@ constructor( } } } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 300L + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index 728bafae2a4c..37f33afbf53e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -42,6 +42,8 @@ abstract class StartKeyguardTransitionModule { @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt new file mode 100644 index 000000000000..db709b476c5d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard.shared.model + +/** Model device wakefulness states. */ +enum class BiometricUnlockModel { + /** Mode in which we don't need to wake up the device when we authenticate. */ + NONE, + /** + * Mode in which we wake up the device, and directly dismiss Keyguard. Active when we acquire a + * fingerprint while the screen is off and the device was sleeping. + */ + WAKE_AND_UNLOCK, + /** + * Mode in which we wake the device up, and fade out the Keyguard contents because they were + * already visible while pulsing in doze mode. + */ + WAKE_AND_UNLOCK_PULSING, + /** + * Mode in which we wake up the device, but play the normal dismiss animation. Active when we + * acquire a fingerprint pulsing in doze mode. + */ + SHOW_BOUNCER, + /** + * Mode in which we only wake up the device, and keyguard was not showing when we authenticated. + */ + ONLY_WAKE, + /** + * Mode in which fingerprint unlocks the device or passive auth (ie face auth) unlocks the + * device while being requested when keyguard is occluded or showing. + */ + UNLOCK_COLLAPSING, + /** When bouncer is visible and will be dismissed. */ + DISMISS_BOUNCER, + /** Mode in which fingerprint wakes and unlocks the device from a dream. */ + WAKE_AND_UNLOCK_FROM_DREAM, +} diff --git a/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt index fd99b21c9538..a56bc900f936 100644 --- a/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * 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. @@ -11,21 +11,20 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License + * limitations under the License. + * */ -package com.android.server.appwidget; +package com.android.systemui.keyguard.shared.model -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +import androidx.annotation.DrawableRes /** - * Placeholder widget for testing + * Representation of a quick affordance for use to build "picker", "selector", or "settings" + * experiences. */ -public class DummyAppWidget extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - } -} +data class KeyguardQuickAffordancePickerRepresentation( + val id: String, + val name: String, + @DrawableRes val iconResourceId: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt new file mode 100644 index 000000000000..86f27567ddf8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.shared.model + +/** + * Representation of a quick affordance slot (or position) for use to build "picker", "selector", or + * "settings" experiences. + */ +data class KeyguardSlotPickerRepresentation( + val id: String, + /** The maximum number of selected affordances that can be present on this slot. */ + val maxSelectedAffordances: Int = 1, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt index 0ca358210813..732a6f7b887a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -21,10 +21,11 @@ data class TransitionStep( val to: KeyguardState = KeyguardState.NONE, val value: Float = 0f, // constrained [0.0, 1.0] val transitionState: TransitionState = TransitionState.FINISHED, + val ownerName: String = "", ) { constructor( info: TransitionInfo, value: Float, transitionState: TransitionState, - ) : this(info.from, info.to, value, transitionState) + ) : this(info.from, info.to, value, transitionState, info.ownerName) } diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt new file mode 100644 index 000000000000..1324d2c5c27b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolModule.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.motiontool + +import android.view.WindowManagerGlobal +import com.android.app.motiontool.DdmHandleMotionTool +import com.android.app.motiontool.MotionToolManager +import com.android.app.viewcapture.ViewCapture +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface MotionToolModule { + + companion object { + + @Provides + fun provideDdmHandleMotionTool(motionToolManager: MotionToolManager): DdmHandleMotionTool { + return DdmHandleMotionTool.getInstance(motionToolManager) + } + + @Provides + fun provideMotionToolManager( + viewCapture: ViewCapture, + windowManagerGlobal: WindowManagerGlobal + ): MotionToolManager { + return MotionToolManager.getInstance(viewCapture, windowManagerGlobal) + } + + @Provides + fun provideWindowManagerGlobal(): WindowManagerGlobal = WindowManagerGlobal.getInstance() + + @Provides fun provideViewCapture(): ViewCapture = ViewCapture.getInstance() + } + + @Binds + @IntoMap + @ClassKey(MotionToolStartable::class) + fun bindMotionToolStartable(impl: MotionToolStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt new file mode 100644 index 000000000000..fbb9538316f7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/motiontool/MotionToolStartable.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.motiontool + +import com.android.app.motiontool.DdmHandleMotionTool +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +@SysUISingleton +class MotionToolStartable +@Inject +internal constructor(private val ddmHandleMotionTool: DdmHandleMotionTool) : CoreStartable { + + override fun start() { + ddmHandleMotionTool.register() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java index 258f9fc5449f..c070fccf9808 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java @@ -19,6 +19,7 @@ package com.android.systemui.statusbar; import android.annotation.NonNull; import android.annotation.Nullable; import android.media.AudioAttributes; +import android.os.Process; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; @@ -33,6 +34,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; /** + * */ @SysUISingleton public class VibratorHelper { @@ -40,9 +42,18 @@ public class VibratorHelper { private final Vibrator mVibrator; public static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH); + + private static final VibrationEffect BIOMETRIC_SUCCESS_VIBRATION_EFFECT = + VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + private static final VibrationEffect BIOMETRIC_ERROR_VIBRATION_EFFECT = + VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); + private final Executor mExecutor; /** + * */ @Inject public VibratorHelper(@Nullable Vibrator vibrator, @Background Executor executor) { @@ -109,4 +120,23 @@ public class VibratorHelper { } mExecutor.execute(mVibrator::cancel); } + + /** + * Perform vibration when biometric authentication success + */ + public void vibrateAuthSuccess(String reason) { + vibrate(Process.myUid(), + "com.android.systemui", + BIOMETRIC_SUCCESS_VIBRATION_EFFECT, reason, + HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); + } + + /** + * Perform vibration when biometric authentication error + */ + public void vibrateAuthError(String reason) { + vibrate(Process.myUid(), "com.android.systemui", + BIOMETRIC_ERROR_VIBRATION_EFFECT, reason, + HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 470cbcb842c4..5dbb4f9d70df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -33,6 +33,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.dagger.IncomingHeader import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder @@ -68,6 +69,7 @@ class HeadsUpCoordinator @Inject constructor( private val mHeadsUpViewBinder: HeadsUpViewBinder, private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider, private val mRemoteInputManager: NotificationRemoteInputManager, + private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider, @IncomingHeader private val mIncomingHeaderController: NodeController, @Main private val mExecutor: DelayableExecutor, ) : Coordinator { @@ -380,6 +382,12 @@ class HeadsUpCoordinator @Inject constructor( * Notification was just added and if it should heads up, bind the view and then show it. */ override fun onEntryAdded(entry: NotificationEntry) { + // First check whether this notification should launch a full screen intent, and + // launch it if needed. + if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) { + mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry) + } + // shouldHeadsUp includes check for whether this notification should be filtered val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry) mPostedEntries[entry.key] = PostedEntry( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt new file mode 100644 index 000000000000..74ff78e25a2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.util.ListenerSet +import javax.inject.Inject + +/** + * A class that enables communication of decisions to launch a notification's full screen intent. + */ +@SysUISingleton +class LaunchFullScreenIntentProvider @Inject constructor() { + companion object { + private const val TAG = "LaunchFullScreenIntentProvider" + } + private val listeners = ListenerSet<Listener>() + + /** + * Registers a listener with this provider. These listeners will be alerted whenever a full + * screen intent should be launched for a notification entry. + */ + fun registerListener(listener: Listener) { + listeners.addIfAbsent(listener) + } + + /** Removes the specified listener. */ + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + /** + * Sends a request to launch full screen intent for the given notification entry to all + * registered listeners. + */ + fun launchFullScreenIntent(entry: NotificationEntry) { + if (listeners.isEmpty()) { + // This should never happen, but we should definitely know if it does because having + // no listeners would indicate that FSIs are getting entirely dropped on the floor. + Log.wtf(TAG, "no listeners found when launchFullScreenIntent requested") + } + for (listener in listeners) { + listener.onFullScreenIntentRequested(entry) + } + } + + /** Listener interface for passing full screen intent launch decisions. */ + fun interface Listener { + /** + * Invoked whenever a full screen intent launch is requested for the given notification + * entry. + */ + fun onFullScreenIntentRequested(entry: NotificationEntry) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index a2798f47be65..6e68079706f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -27,11 +27,8 @@ import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; -import android.os.Process; import android.os.SystemClock; import android.os.Trace; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; import android.util.Log; import androidx.annotation.Nullable; @@ -70,8 +67,10 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.inject.Inject; @@ -87,12 +86,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); private static final int UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER = 3; - private static final VibrationEffect SUCCESS_VIBRATION_EFFECT = - VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - private static final VibrationEffect ERROR_VIBRATION_EFFECT = - VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); - private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = - VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); @IntDef(prefix = { "MODE_" }, value = { MODE_NONE, @@ -167,7 +160,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final NotificationShadeWindowController mNotificationShadeWindowController; private final SessionTracker mSessionTracker; private final int mConsecutiveFpFailureThreshold; - private final boolean mShouldVibrate; private int mMode; private BiometricSourceType mBiometricType; private KeyguardViewController mKeyguardViewController; @@ -177,7 +169,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private PendingAuthenticated mPendingAuthenticated = null; private boolean mHasScreenTurnedOnSinceAuthenticating; private boolean mFadedAwayAfterWakeAndUnlock; - private BiometricModeListener mBiometricModeListener; + private Set<BiometricModeListener> mBiometricModeListeners = new HashSet<>(); private final MetricsLogger mMetricsLogger; private final AuthController mAuthController; @@ -307,8 +299,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mHandler = handler; mConsecutiveFpFailureThreshold = resources.getInteger( R.integer.fp_consecutive_failure_time_ms); - mShouldVibrate = !(resources.getBoolean( - com.android.internal.R.bool.system_server_plays_face_haptics)); mKeyguardBypassController = keyguardBypassController; mKeyguardBypassController.setUnlockController(this); mMetricsLogger = metricsLogger; @@ -326,9 +316,14 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mKeyguardViewController = keyguardViewController; } - /** Sets a {@link BiometricModeListener}. */ - public void setBiometricModeListener(BiometricModeListener biometricModeListener) { - mBiometricModeListener = biometricModeListener; + /** Adds a {@link BiometricModeListener}. */ + public void addBiometricModeListener(BiometricModeListener listener) { + mBiometricModeListeners.add(listener); + } + + /** Removes a {@link BiometricModeListener}. */ + public void removeBiometricModeListener(BiometricModeListener listener) { + mBiometricModeListeners.remove(listener); } private final Runnable mReleaseBiometricWakeLockRunnable = new Runnable() { @@ -423,10 +418,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp public void startWakeAndUnlock(BiometricSourceType biometricSourceType, boolean isStrongBiometric) { int mode = calculateMode(biometricSourceType, isStrongBiometric); - if (BiometricSourceType.FACE == biometricSourceType && (mode == MODE_WAKE_AND_UNLOCK + if (mode == MODE_WAKE_AND_UNLOCK || mode == MODE_WAKE_AND_UNLOCK_PULSING || mode == MODE_UNLOCK_COLLAPSING - || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER)) { - vibrateSuccess(); + || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER) { + vibrateSuccess(biometricSourceType); } startWakeAndUnlock(mode); } @@ -511,15 +506,12 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp break; } onModeChanged(mMode); - if (mBiometricModeListener != null) { - mBiometricModeListener.notifyBiometricAuthModeChanged(); - } Trace.endSection(); } private void onModeChanged(@WakeAndUnlockMode int mode) { - if (mBiometricModeListener != null) { - mBiometricModeListener.onModeChanged(mode); + for (BiometricModeListener listener : mBiometricModeListeners) { + listener.onModeChanged(mode); } } @@ -659,10 +651,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } // Suppress all face auth errors if fingerprint can be used to authenticate - if (biometricSourceType == BiometricSourceType.FACE + if ((biometricSourceType == BiometricSourceType.FACE && !mUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( - KeyguardUpdateMonitor.getCurrentUser())) { - vibrateError(); + KeyguardUpdateMonitor.getCurrentUser())) + || (biometricSourceType == BiometricSourceType.FINGERPRINT)) { + vibrateError(biometricSourceType); } cleanup(); @@ -688,24 +681,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - private void vibrateSuccess() { - if (mShouldVibrate) { - mVibratorHelper.vibrate(Process.myUid(), - "com.android.systemui", - SUCCESS_VIBRATION_EFFECT, - getClass().getSimpleName() + "::success", - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); - } + //these haptics are for device-entry only + private void vibrateSuccess(BiometricSourceType type) { + mVibratorHelper.vibrateAuthSuccess( + getClass().getSimpleName() + ", type =" + type + "device-entry::success"); } - private void vibrateError() { - if (mShouldVibrate) { - mVibratorHelper.vibrate(Process.myUid(), - "com.android.systemui", - ERROR_VIBRATION_EFFECT, - getClass().getSimpleName() + "::error", - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); - } + private void vibrateError(BiometricSourceType type) { + mVibratorHelper.vibrateAuthError( + getClass().getSimpleName() + ", type =" + type + "device-entry::error"); } private void cleanup() { @@ -734,9 +718,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mMode = MODE_NONE; mBiometricType = null; mNotificationShadeWindowController.setForceDozeBrightness(false); - if (mBiometricModeListener != null) { - mBiometricModeListener.onResetMode(); - mBiometricModeListener.notifyBiometricAuthModeChanged(); + for (BiometricModeListener listener : mBiometricModeListeners) { + listener.onResetMode(); } mNumConsecutiveFpFailures = 0; mLastFpFailureUptimeMillis = 0; @@ -845,10 +828,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp /** An interface to interact with the {@link BiometricUnlockController}. */ public interface BiometricModeListener { /** Called when {@code mMode} is reset to {@link #MODE_NONE}. */ - void onResetMode(); + default void onResetMode() {} /** Called when {@code mMode} has changed in {@link #startWakeAndUnlock(int)}. */ - void onModeChanged(@WakeAndUnlockMode int mode); - /** Called after processing {@link #onModeChanged(int)}. */ - void notifyBiometricAuthModeChanged(); + default void onModeChanged(@WakeAndUnlockMode int mode) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index be3052e3b3d1..15cc086586da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1527,11 +1527,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected void startKeyguard() { Trace.beginSection("CentralSurfaces#startKeyguard"); mBiometricUnlockController = mBiometricUnlockControllerLazy.get(); - mBiometricUnlockController.setBiometricModeListener( + mBiometricUnlockController.addBiometricModeListener( new BiometricUnlockController.BiometricModeListener() { @Override public void onResetMode() { setWakeAndUnlocking(false); + notifyBiometricAuthModeChanged(); } @Override @@ -1542,11 +1543,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { case BiometricUnlockController.MODE_WAKE_AND_UNLOCK: setWakeAndUnlocking(true); } - } - - @Override - public void notifyBiometricAuthModeChanged() { - CentralSurfacesImpl.this.notifyBiometricAuthModeChanged(); + notifyBiometricAuthModeChanged(); } private void setWakeAndUnlocking(boolean wakeAndUnlocking) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index 86f6ff850409..0a0ded24ef30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -18,6 +18,7 @@ import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI; +import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW; import android.annotation.Nullable; import android.content.Context; @@ -53,8 +54,9 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; -import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; import com.android.systemui.util.Assert; import java.util.ArrayList; @@ -84,7 +86,18 @@ public interface StatusBarIconController { /** */ void setIcon(String slot, StatusBarIcon icon); /** */ - void setSignalIcon(String slot, WifiIconState state); + void setWifiIcon(String slot, WifiIconState state); + + /** + * Sets up a wifi icon using the new data pipeline. No effect if the wifi icon has already been + * set up (inflated and added to the view hierarchy). + * + * This method completely replaces {@link #setWifiIcon} with the information from the new wifi + * data pipeline. Icons will automatically keep their state up to date, so we don't have to + * worry about funneling state objects through anymore. + */ + void setNewWifiIcon(); + /** */ void setMobileIcons(String slot, List<MobileIconState> states); @@ -151,14 +164,14 @@ public interface StatusBarIconController { LinearLayout linearLayout, StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, DarkIconDispatcher darkIconDispatcher) { super(linearLayout, location, statusBarPipelineFlags, - wifiViewModel, + wifiUiAdapter, mobileUiAdapter, mobileContextProvider); mIconHPadding = mContext.getResources().getDimensionPixelSize( @@ -218,7 +231,7 @@ public interface StatusBarIconController { @SysUISingleton public static class Factory { private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final WifiViewModel mWifiViewModel; + private final WifiUiAdapter mWifiUiAdapter; private final MobileContextProvider mMobileContextProvider; private final MobileUiAdapter mMobileUiAdapter; private final DarkIconDispatcher mDarkIconDispatcher; @@ -226,12 +239,12 @@ public interface StatusBarIconController { @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileContextProvider mobileContextProvider, MobileUiAdapter mobileUiAdapter, DarkIconDispatcher darkIconDispatcher) { mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModel = wifiViewModel; + mWifiUiAdapter = wifiUiAdapter; mMobileContextProvider = mobileContextProvider; mMobileUiAdapter = mobileUiAdapter; mDarkIconDispatcher = darkIconDispatcher; @@ -242,7 +255,7 @@ public interface StatusBarIconController { group, location, mStatusBarPipelineFlags, - mWifiViewModel, + mWifiUiAdapter, mMobileUiAdapter, mMobileContextProvider, mDarkIconDispatcher); @@ -260,14 +273,14 @@ public interface StatusBarIconController { ViewGroup group, StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider ) { super(group, location, statusBarPipelineFlags, - wifiViewModel, + wifiUiAdapter, mobileUiAdapter, mobileContextProvider); } @@ -302,19 +315,19 @@ public interface StatusBarIconController { @SysUISingleton public static class Factory { private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final WifiViewModel mWifiViewModel; + private final WifiUiAdapter mWifiUiAdapter; private final MobileContextProvider mMobileContextProvider; private final MobileUiAdapter mMobileUiAdapter; @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider ) { mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModel = wifiViewModel; + mWifiUiAdapter = wifiUiAdapter; mMobileUiAdapter = mobileUiAdapter; mMobileContextProvider = mobileContextProvider; } @@ -324,7 +337,7 @@ public interface StatusBarIconController { group, location, mStatusBarPipelineFlags, - mWifiViewModel, + mWifiUiAdapter, mMobileUiAdapter, mMobileContextProvider); } @@ -336,10 +349,9 @@ public interface StatusBarIconController { */ class IconManager implements DemoModeCommandReceiver { protected final ViewGroup mGroup; - private final StatusBarLocation mLocation; private final StatusBarPipelineFlags mStatusBarPipelineFlags; - private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final LocationBasedWifiViewModel mWifiViewModel; private final MobileIconsViewModel mMobileIconsViewModel; protected final Context mContext; @@ -359,26 +371,33 @@ public interface StatusBarIconController { ViewGroup group, StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider ) { mGroup = group; - mLocation = location; mStatusBarPipelineFlags = statusBarPipelineFlags; - mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); - if (statusBarPipelineFlags.useNewMobileIcons()) { - // This starts the flow for the new pipeline, and will notify us of changes + if (statusBarPipelineFlags.runNewMobileIconsBackend()) { + // This starts the flow for the new pipeline, and will notify us of changes if + // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true. mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); } else { mMobileIconsViewModel = null; } + + if (statusBarPipelineFlags.runNewWifiIconBackend()) { + // This starts the flow for the new pipeline, and will notify us of changes if + // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true. + mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location); + } else { + mWifiViewModel = null; + } } public boolean isDemoable() { @@ -429,6 +448,9 @@ public interface StatusBarIconController { case TYPE_WIFI: return addWifiIcon(index, slot, holder.getWifiState()); + case TYPE_WIFI_NEW: + return addNewWifiIcon(index, slot); + case TYPE_MOBILE: return addMobileIcon(index, slot, holder.getMobileState()); @@ -450,16 +472,13 @@ public interface StatusBarIconController { @VisibleForTesting protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) { - final BaseStatusBarFrameLayout view; if (mStatusBarPipelineFlags.useNewWifiIcon()) { - view = onCreateModernStatusBarWifiView(slot); - // When [ModernStatusBarWifiView] is created, it will automatically apply the - // correct view state so we don't need to call applyWifiState. - } else { - StatusBarWifiView wifiView = onCreateStatusBarWifiView(slot); - wifiView.applyWifiState(state); - view = wifiView; + throw new IllegalStateException("Attempting to add a mobile icon while the new " + + "icons are enabled is not supported"); } + + final StatusBarWifiView view = onCreateStatusBarWifiView(slot); + view.applyWifiState(state); mGroup.addView(view, index, onCreateLayoutParams()); if (mIsInDemoMode) { @@ -468,6 +487,17 @@ public interface StatusBarIconController { return view; } + protected StatusIconDisplayable addNewWifiIcon(int index, String slot) { + if (!mStatusBarPipelineFlags.useNewWifiIcon()) { + throw new IllegalStateException("Attempting to add a wifi icon using the new" + + "pipeline, but the enabled flag is false."); + } + + ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); + mGroup.addView(view, index, onCreateLayoutParams()); + return view; + } + @VisibleForTesting protected StatusIconDisplayable addMobileIcon( int index, @@ -523,8 +553,7 @@ public interface StatusBarIconController { } private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) { - return ModernStatusBarWifiView.constructAndBind( - mContext, slot, mWifiViewModel, mLocation); + return ModernStatusBarWifiView.constructAndBind(mContext, slot, mWifiViewModel); } private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) { @@ -600,7 +629,8 @@ public interface StatusBarIconController { onSetMobileIcon(viewIndex, holder.getMobileState()); return; case TYPE_MOBILE_NEW: - // Nothing, the icon updates itself now + case TYPE_WIFI_NEW: + // Nothing, the new icons update themselves return; default: break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 31e960ad7d69..674e5747e331 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -195,12 +195,13 @@ public class StatusBarIconControllerImpl implements Tunable, } } - /** - * Signal icons need to be handled differently, because they can be - * composite views - */ @Override - public void setSignalIcon(String slot, WifiIconState state) { + public void setWifiIcon(String slot, WifiIconState state) { + if (mStatusBarPipelineFlags.useNewWifiIcon()) { + Log.d(TAG, "ignoring old pipeline callback because the new wifi icon is enabled"); + return; + } + if (state == null) { removeIcon(slot, 0); return; @@ -216,6 +217,24 @@ public class StatusBarIconControllerImpl implements Tunable, } } + + @Override + public void setNewWifiIcon() { + if (!mStatusBarPipelineFlags.useNewWifiIcon()) { + Log.d(TAG, "ignoring new pipeline callback because the new wifi icon is disabled"); + return; + } + + String slot = mContext.getString(com.android.internal.R.string.status_bar_wifi); + StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, /* tag= */ 0); + if (holder == null) { + holder = StatusBarIconHolder.forNewWifiIcon(); + setIcon(slot, holder); + } else { + // Don't have to do anything in the new world + } + } + /** * Accept a list of MobileIconStates, which all live in the same slot(?!), and then are sorted * by subId. Don't worry this definitely makes sense and works. @@ -225,7 +244,7 @@ public class StatusBarIconControllerImpl implements Tunable, @Override public void setMobileIcons(String slot, List<MobileIconState> iconStates) { if (mStatusBarPipelineFlags.useNewMobileIcons()) { - Log.d(TAG, "ignoring old pipeline callbacks, because the new " + Log.d(TAG, "ignoring old pipeline callbacks, because the new mobile " + "icons are enabled"); return; } @@ -251,10 +270,11 @@ public class StatusBarIconControllerImpl implements Tunable, public void setNewMobileIconSubIds(List<Integer> subIds) { if (!mStatusBarPipelineFlags.useNewMobileIcons()) { Log.d(TAG, "ignoring new pipeline callback, " - + "since the new icons are disabled"); + + "since the new mobile icons are disabled"); return; } - Slot mobileSlot = mStatusBarIconList.getSlot("mobile"); + String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile); + Slot mobileSlot = mStatusBarIconList.getSlot(slotName); Collections.reverse(subIds); @@ -262,7 +282,7 @@ public class StatusBarIconControllerImpl implements Tunable, StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId); if (holder == null) { holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId); - setIcon("mobile", holder); + setIcon(slotName, holder); } else { // Don't have to do anything in the new world } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index 68a203e30f98..f6c0da8da8c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -51,11 +51,24 @@ public class StatusBarIconHolder { @Deprecated public static final int TYPE_MOBILE_NEW = 3; + /** + * TODO (b/238425913): address this once the new pipeline is in place + * This type exists so that the new wifi pipeline can be used to inform the old view system + * about the existence of the wifi icon. The design of the new pipeline should allow for removal + * of this icon holder type, and obsolete the need for this entire class. + * + * @deprecated This field only exists so the new status bar pipeline can interface with the + * view holder system. + */ + @Deprecated + public static final int TYPE_WIFI_NEW = 4; + @IntDef({ TYPE_ICON, TYPE_WIFI, TYPE_MOBILE, - TYPE_MOBILE_NEW + TYPE_MOBILE_NEW, + TYPE_WIFI_NEW }) @Retention(RetentionPolicy.SOURCE) @interface IconType {} @@ -95,6 +108,13 @@ public class StatusBarIconHolder { return holder; } + /** Creates a new holder with for the new wifi icon. */ + public static StatusBarIconHolder forNewWifiIcon() { + StatusBarIconHolder holder = new StatusBarIconHolder(); + holder.mType = TYPE_WIFI_NEW; + return holder; + } + /** */ public static StatusBarIconHolder fromMobileIconState(MobileIconState state) { StatusBarIconHolder holder = new StatusBarIconHolder(); @@ -172,9 +192,10 @@ public class StatusBarIconHolder { case TYPE_MOBILE: return mMobileState.visible; case TYPE_MOBILE_NEW: - //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + case TYPE_WIFI_NEW: + // The new pipeline controls visibilities via the view model and view binder, so + // this is effectively an unused return value. return true; - default: return true; } @@ -199,7 +220,9 @@ public class StatusBarIconHolder { break; case TYPE_MOBILE_NEW: - //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + case TYPE_WIFI_NEW: + // The new pipeline controls visibilities via the view model and view binder, so + // ignore setVisible. break; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 5cd2ba1b1cf3..b6ae4a088880 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -23,7 +23,6 @@ import static com.android.systemui.statusbar.phone.CentralSurfaces.getActivityOp import android.app.ActivityManager; import android.app.KeyguardManager; import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.content.Context; @@ -60,9 +59,8 @@ import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -126,7 +124,6 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte Context context, Handler mainThreadHandler, Executor uiBgExecutor, - NotifPipeline notifPipeline, NotificationVisibilityProvider visibilityProvider, HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, @@ -151,7 +148,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte NotificationPresenter presenter, NotificationPanelViewController panel, ActivityLaunchAnimator activityLaunchAnimator, - NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) { + NotificationLaunchAnimatorControllerProvider notificationAnimationProvider, + LaunchFullScreenIntentProvider launchFullScreenIntentProvider) { mContext = context; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; @@ -182,12 +180,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mActivityLaunchAnimator = activityLaunchAnimator; mNotificationAnimationProvider = notificationAnimationProvider; - notifPipeline.addCollectionListener(new NotifCollectionListener() { - @Override - public void onEntryAdded(NotificationEntry entry) { - handleFullScreenIntent(entry); - } - }); + launchFullScreenIntentProvider.registerListener(entry -> launchFullScreenIntent(entry)); } /** @@ -549,38 +542,36 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte } @VisibleForTesting - void handleFullScreenIntent(NotificationEntry entry) { - if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) { - if (shouldSuppressFullScreenIntent(entry)) { - mLogger.logFullScreenIntentSuppressedByDnD(entry); - } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - mLogger.logFullScreenIntentNotImportantEnough(entry); - } else { - // Stop screensaver if the notification has a fullscreen intent. - // (like an incoming phone call) - mUiBgExecutor.execute(() -> { - try { - mDreamManager.awaken(); - } catch (RemoteException e) { - e.printStackTrace(); - } - }); + void launchFullScreenIntent(NotificationEntry entry) { + // Skip if device is in VR mode. + if (mPresenter.isDeviceInVrMode()) { + mLogger.logFullScreenIntentSuppressedByVR(entry); + return; + } - // not immersive & a fullscreen alert should be shown - final PendingIntent fullscreenIntent = - entry.getSbn().getNotification().fullScreenIntent; - mLogger.logSendingFullScreenIntent(entry, fullscreenIntent); - try { - EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, - entry.getKey()); - mCentralSurfaces.wakeUpForFullScreenIntent(); - fullscreenIntent.send(); - entry.notifyFullScreenIntentLaunched(); - mMetricsLogger.count("note_fullscreen", 1); - } catch (PendingIntent.CanceledException e) { - // ignore - } + // Stop screensaver if the notification has a fullscreen intent. + // (like an incoming phone call) + mUiBgExecutor.execute(() -> { + try { + mDreamManager.awaken(); + } catch (RemoteException e) { + e.printStackTrace(); } + }); + + // not immersive & a fullscreen alert should be shown + final PendingIntent fullscreenIntent = + entry.getSbn().getNotification().fullScreenIntent; + mLogger.logSendingFullScreenIntent(entry, fullscreenIntent); + try { + EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, + entry.getKey()); + mCentralSurfaces.wakeUpForFullScreenIntent(); + fullscreenIntent.send(); + entry.notifyFullScreenIntentLaunched(); + mMetricsLogger.count("note_fullscreen", 1); + } catch (PendingIntent.CanceledException e) { + // ignore } } @@ -607,12 +598,4 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mMainThreadHandler.post(mShadeController::collapsePanel); } } - - private boolean shouldSuppressFullScreenIntent(NotificationEntry entry) { - if (mPresenter.isDeviceInVrMode()) { - return true; - } - - return entry.shouldSuppressFullScreenIntent(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt index 81edff45c505..1f0b96a58da6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -96,19 +96,11 @@ class StatusBarNotificationActivityStarterLogger @Inject constructor( }) } - fun logFullScreenIntentSuppressedByDnD(entry: NotificationEntry) { + fun logFullScreenIntentSuppressedByVR(entry: NotificationEntry) { buffer.log(TAG, DEBUG, { str1 = entry.logKey }, { - "No Fullscreen intent: suppressed by DND: $str1" - }) - } - - fun logFullScreenIntentNotImportantEnough(entry: NotificationEntry) { - buffer.log(TAG, DEBUG, { - str1 = entry.logKey - }, { - "No Fullscreen intent: not important enough: $str1" + "No Fullscreen intent: suppressed by VR mode: $str1" }) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java index 492734e93dca..de7bf3c021dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java @@ -212,7 +212,7 @@ public class StatusBarSignalPolicy implements SignalCallback, private void updateWifiIconWithState(WifiIconState state) { if (DEBUG) Log.d(TAG, "WifiIconState: " + state == null ? "" : state.toString()); if (state.visible && state.resId > 0) { - mIconController.setSignalIcon(mSlotWifi, state); + mIconController.setWifiIcon(mSlotWifi, state); mIconController.setIconVisibility(mSlotWifi, true); } else { mIconController.setIconVisibility(mSlotWifi, false); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 06cd12dd1a0d..946d7e4a3e75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -27,11 +27,26 @@ class StatusBarPipelineFlags @Inject constructor(private val featureFlags: Featu /** True if we should display the mobile icons using the new status bar data pipeline. */ fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS) + /** + * True if we should run the new mobile icons backend to get the logging. + * + * Does *not* affect whether we render the mobile icons using the new backend data. See + * [useNewMobileIcons] for that. + */ + fun runNewMobileIconsBackend(): Boolean = + featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS_BACKEND) || useNewMobileIcons() + /** True if we should display the wifi icon using the new status bar data pipeline. */ fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON) - // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the - // logging without getting the UI effects. + /** + * True if we should run the new wifi icon backend to get the logging. + * + * Does *not* affect whether we render the wifi icon using the new backend data. See + * [useNewWifiIcon] for that. + */ + fun runNewWifiIconBackend(): Boolean = + featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON_BACKEND) || useNewWifiIcon() /** * Returns true if we should apply some coloring to the wifi icon that was rendered with the new diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index 380017cd3418..c7e0ce173ece 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import javax.inject.Inject @@ -50,6 +51,7 @@ constructor( private val iconController: StatusBarIconController, private val iconsViewModelFactory: MobileIconsViewModel.Factory, @Application scope: CoroutineScope, + private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { private val mobileSubIds: Flow<List<Int>> = interactor.filteredSubscriptions.mapLatest { infos -> @@ -66,8 +68,14 @@ constructor( private val mobileSubIdsState: StateFlow<List<Int>> = mobileSubIds .onEach { - // Notify the icon controller here so that it knows to add icons - iconController.setNewMobileIconSubIds(it) + // Only notify the icon controller if we want to *render* the new icons. + // Note that this flow may still run if + // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to + // get the logging data without rendering. + if (statusBarPipelineFlags.useNewMobileIcons()) { + // Notify the icon controller here so that it knows to add icons + iconController.setNewMobileIconSubIds(it) + } } .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt new file mode 100644 index 000000000000..b816364ed4cf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.ui + +import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.phone.StatusBarIconController +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** + * This class serves as a bridge between the old UI classes and the new data pipeline. + * + * Once the new pipeline notifies [wifiViewModel] that the wifi icon should be visible, this class + * notifies [iconController] to inflate the wifi icon (if needed). After that, the [wifiViewModel] + * has sole responsibility for updating the wifi icon drawable, visibility, etc. and the + * [iconController] will not do any updates to the icon. + */ +@SysUISingleton +class WifiUiAdapter +@Inject +constructor( + private val iconController: StatusBarIconController, + private val wifiViewModel: WifiViewModel, + private val statusBarPipelineFlags: StatusBarPipelineFlags, +) { + /** + * Binds the container for all the status bar icons to a view model, so that we inflate the wifi + * view once we receive a valid icon from the data pipeline. + * + * NOTE: This should go away as we better integrate the data pipeline with the UI. + * + * @return the view model used for this particular group in the given [location]. + */ + fun bindGroup( + statusBarIconGroup: ViewGroup, + location: StatusBarLocation, + ): LocationBasedWifiViewModel { + val locationViewModel = + when (location) { + StatusBarLocation.HOME -> wifiViewModel.home + StatusBarLocation.KEYGUARD -> wifiViewModel.keyguard + StatusBarLocation.QS -> wifiViewModel.qs + } + + statusBarIconGroup.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + locationViewModel.wifiIcon.collect { wifiIcon -> + // Only notify the icon controller if we want to *render* the new icon. + // Note that this flow may still run if + // [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may + // want to get the logging data without rendering. + if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) { + iconController.setNewWifiIcon() + } + } + } + } + } + + return locationViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 25537b948517..345f8cb75660 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -30,9 +30,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON -import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel -import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect @@ -62,26 +60,9 @@ object WifiViewBinder { fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) } - /** - * Binds the view to the appropriate view-model based on the given location. The view will - * continue to be updated following updates from the view-model. - */ - @JvmStatic - fun bind( - view: ViewGroup, - wifiViewModel: WifiViewModel, - location: StatusBarLocation, - ): Binding { - return when (location) { - StatusBarLocation.HOME -> bind(view, wifiViewModel.home) - StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard) - StatusBarLocation.QS -> bind(view, wifiViewModel.qs) - } - } - /** Binds the view to the view-model, continuing to update the former based on the latter. */ @JvmStatic - private fun bind( + fun bind( view: ViewGroup, viewModel: LocationBasedWifiViewModel, ): Binding { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index 0cd9bd7d97b0..a45076b53356 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -26,9 +26,8 @@ import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN -import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder -import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel /** * A new and more modern implementation of [com.android.systemui.statusbar.StatusBarWifiView] that @@ -81,12 +80,11 @@ class ModernStatusBarWifiView( private fun initView( slotName: String, - wifiViewModel: WifiViewModel, - location: StatusBarLocation, + wifiViewModel: LocationBasedWifiViewModel, ) { slot = slotName initDotView() - binding = WifiViewBinder.bind(this, wifiViewModel, location) + binding = WifiViewBinder.bind(this, wifiViewModel) } // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView]. @@ -116,14 +114,13 @@ class ModernStatusBarWifiView( fun constructAndBind( context: Context, slot: String, - wifiViewModel: WifiViewModel, - location: StatusBarLocation, + wifiViewModel: LocationBasedWifiViewModel, ): ModernStatusBarWifiView { return ( LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) as ModernStatusBarWifiView ).also { - it.initView(slot, wifiViewModel, location) + it.initView(slot, wifiViewModel) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 89b96b7bc75d..0782bbb774eb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -145,7 +145,8 @@ constructor( else -> null } } - .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null) + .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null) /** The wifi activity status. Null if we shouldn't display the activity status. */ private val activity: Flow<WifiActivityModel?> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java index 1d414745e6ed..7acdaffb48c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceStateRotationLockSettingController.java @@ -118,7 +118,10 @@ public final class DeviceStateRotationLockSettingController private void updateDeviceState(int state) { Log.v(TAG, "updateDeviceState [state=" + state + "]"); - Trace.beginSection("updateDeviceState [state=" + state + "]"); + if (Trace.isEnabled()) { + Trace.traceBegin( + Trace.TRACE_TAG_APP, "updateDeviceState [state=" + state + "]"); + } try { if (mDeviceState == state) { return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java index bd2123a251c3..69b55c81f48b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java @@ -33,6 +33,7 @@ import android.util.Log; import androidx.annotation.NonNull; import com.android.internal.util.ConcurrentUtils; +import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -63,6 +64,7 @@ public class HotspotControllerImpl implements HotspotController, WifiManager.Sof private volatile int mNumConnectedDevices; // Assume tethering is available until told otherwise private volatile boolean mIsTetheringSupported = true; + private final boolean mIsTetheringSupportedConfig; private volatile boolean mHasTetherableWifiRegexs = true; private boolean mWaitingForTerminalState; @@ -100,23 +102,29 @@ public class HotspotControllerImpl implements HotspotController, WifiManager.Sof mTetheringManager = context.getSystemService(TetheringManager.class); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mMainHandler = mainHandler; - mTetheringManager.registerTetheringEventCallback( - new HandlerExecutor(backgroundHandler), mTetheringCallback); + mIsTetheringSupportedConfig = context.getResources() + .getBoolean(R.bool.config_show_wifi_tethering); + if (mIsTetheringSupportedConfig) { + mTetheringManager.registerTetheringEventCallback( + new HandlerExecutor(backgroundHandler), mTetheringCallback); + } dumpManager.registerDumpable(getClass().getSimpleName(), this); } /** * Whether hotspot is currently supported. * - * This will return {@code true} immediately on creation of the controller, but may be updated - * later. Callbacks from this controllers will notify if the state changes. + * This may return {@code true} immediately on creation of the controller, but may be updated + * later as capabilities are collected from System Server. + * + * Callbacks from this controllers will notify if the state changes. * * @return {@code true} if hotspot is supported (or we haven't been told it's not) * @see #addCallback */ @Override public boolean isHotspotSupported() { - return mIsTetheringSupported && mHasTetherableWifiRegexs + return mIsTetheringSupportedConfig && mIsTetheringSupported && mHasTetherableWifiRegexs && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser()); } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt index 07e5cf9d9df2..208061433325 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -28,6 +28,8 @@ import android.util.Log import android.view.WindowManagerGlobal import android.widget.Toast import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -58,6 +60,8 @@ constructor( private val devicePolicyManager: DevicePolicyManager, private val refreshUsersScheduler: RefreshUsersScheduler, private val uiEventLogger: UiEventLogger, + resumeSessionReceiver: GuestResumeSessionReceiver, + resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver, ) { /** Whether the device is configured to always have a guest user available. */ val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated @@ -65,6 +69,11 @@ constructor( /** Whether the guest user is currently being reset. */ val isGuestUserResetting: Boolean = repository.isGuestUserResetting + init { + resumeSessionReceiver.register() + resetOrExitSessionReceiver.register() + } + /** Notifies that the device has finished booting. */ fun onDeviceBootCompleted() { applicationScope.launch { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt index aca60c033bac..131cf7d33e3a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt @@ -72,6 +72,7 @@ private fun fingerprintModel(user: Int) = KeyguardFingerprintListenModel( keyguardOccluded = false, occludingAppRequestingFp = false, primaryUser = false, + shouldListenSfpsState = false, shouldListenForFingerprintAssistant = false, switchingUser = false, udfps = false, diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java index 52f8ef8b7ebc..0a2b3d8498c4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java @@ -194,6 +194,15 @@ public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { } @Test + public void onInitConfiguresViewMode() { + mKeyguardSecurityContainerController.onInit(); + verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), + eq(mUserSwitcherController), + any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), + eq(mFalsingA11yDelegate)); + } + + @Test public void showSecurityScreen_canInflateAllModes() { SecurityMode[] modes = SecurityMode.values(); for (SecurityMode mode : modes) { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 5104f84f3bc6..a8284d29197d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -20,6 +20,7 @@ import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; +import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON; import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE; import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID; @@ -38,6 +39,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -53,6 +55,7 @@ import android.app.trust.IStrongAuthTracker; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -60,18 +63,21 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.pm.UserInfo; +import android.database.ContentObserver; import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback; +import android.hardware.biometrics.SensorProperties; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintManager; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.net.Uri; import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.CancellationSignal; @@ -110,6 +116,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; +import com.android.systemui.util.settings.SecureSettings; import org.junit.After; import org.junit.Assert; @@ -181,6 +188,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock + private SecureSettings mSecureSettings; + @Mock private TelephonyManager mTelephonyManager; @Mock private SensorPrivacyManager mSensorPrivacyManager; @@ -214,6 +223,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private GlobalSettings mGlobalSettings; private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; + private final int mCurrentUserId = 100; private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0); @@ -223,6 +233,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Captor private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor; + @Mock + private Uri mURI; + // Direct executor private final Executor mBackgroundExecutor = Runnable::run; private final Executor mMainExecutor = Runnable::run; @@ -305,6 +318,15 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); + + when(mSecureSettings.getUriFor(anyString())).thenReturn(mURI); + + final ContentResolver contentResolver = mContext.getContentResolver(); + ExtendedMockito.spyOn(contentResolver); + doNothing().when(contentResolver) + .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class), + anyInt()); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); verify(mBiometricManager) @@ -1136,6 +1158,64 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + throws RemoteException { + // SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // WHEN require screen on to auth is disabled, and keyguard is not awake + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0); + mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + + mContext.getOrCreateTestableResources().addOverride( + com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); + + // Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + + statusBarShadeIsLocked(); + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps when screen off, because require screen on is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + + // WHEN require screen on to auth is enabled, and keyguard is not awake + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1); + mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref(); + + // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + + // Device now awake & keyguard is now interactive + deviceNotGoingToSleep(); + deviceIsInteractive(); + keyguardIsVisible(); + + // THEN we should listen for sfps when screen on, and require screen on is enabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } + + + private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( + @FingerprintSensorProperties.SensorType int sensorType) { + return new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + 1 /* maxEnrollmentsPerUser */, + new ArrayList<ComponentInfoInternal>(), + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */); + } + + @Test public void testShouldNotListenForUdfps_whenTrustEnabled() { // GIVEN a "we should listen for udfps" state mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD); @@ -1804,7 +1884,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { protected TestableKeyguardUpdateMonitor(Context context) { super(context, TestableLooper.get(KeyguardUpdateMonitorTest.this).getLooper(), - mBroadcastDispatcher, mDumpManager, + mBroadcastDispatcher, mSecureSettings, mDumpManager, mBackgroundExecutor, mMainExecutor, mStatusBarStateController, mLockPatternUtils, mAuthController, mTelephonyListenerManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index a02dfa3935d7..a275c8d33751 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -87,6 +87,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.Execution; import com.android.systemui.util.concurrency.FakeExecution; @@ -173,6 +174,9 @@ public class AuthControllerTest extends SysuiTestCase { private DelayableExecutor mBackgroundExecutor; private TestableAuthController mAuthController; + @Mock + private VibratorHelper mVibratorHelper; + @Before public void setup() throws RemoteException { mContextSpy = spy(mContext); @@ -221,7 +225,8 @@ public class AuthControllerTest extends SysuiTestCase { mAuthController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, - () -> mUdfpsController, () -> mSidefpsController, mStatusBarStateController); + () -> mUdfpsController, () -> mSidefpsController, mStatusBarStateController, + mVibratorHelper); mAuthController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( @@ -246,11 +251,13 @@ public class AuthControllerTest extends SysuiTestCase { // This test is sensitive to prior FingerprintManager interactions. reset(mFingerprintManager); + when(mVibratorHelper.hasVibrator()).thenReturn(true); + // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, () -> mUdfpsController, () -> mSidefpsController, - mStatusBarStateController); + mStatusBarStateController, mVibratorHelper); authController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( @@ -270,11 +277,13 @@ public class AuthControllerTest extends SysuiTestCase { // This test is sensitive to prior FingerprintManager interactions. reset(mFingerprintManager); + when(mVibratorHelper.hasVibrator()).thenReturn(true); + // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager, () -> mUdfpsController, () -> mSidefpsController, - mStatusBarStateController); + mStatusBarStateController, mVibratorHelper); authController.start(); verify(mFingerprintManager).addAuthenticatorsRegisteredCallback( @@ -928,12 +937,13 @@ public class AuthControllerTest extends SysuiTestCase { FaceManager faceManager, Provider<UdfpsController> udfpsControllerFactory, Provider<SidefpsController> sidefpsControllerFactory, - StatusBarStateController statusBarStateController) { + StatusBarStateController statusBarStateController, + VibratorHelper vibratorHelper) { super(context, execution, commandQueue, activityTaskManager, windowManager, fingerprintManager, faceManager, udfpsControllerFactory, sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle, mUserManager, mLockPatternUtils, statusBarStateController, - mInteractionJankMonitor, mHandler, mBackgroundExecutor); + mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper); } @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index c5a7de410eb0..c234178f8af3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -92,6 +92,12 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { @Mock BouncerCallbackInteractor mBouncerCallbackInteractor; + @Mock + DreamOverlayAnimationsController mAnimationsController; + + @Mock + DreamOverlayStateController mStateController; + DreamOverlayContainerViewController mController; @Before @@ -115,7 +121,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { MAX_BURN_IN_OFFSET, BURN_IN_PROTECTION_UPDATE_INTERVAL, MILLIS_UNTIL_FULL_JITTER, - mBouncerCallbackInteractor); + mBouncerCallbackInteractor, + mAnimationsController, + mStateController); } @Test @@ -188,4 +196,31 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { verify(mBlurUtils).blurRadiusOfRatio(1 - scaledFraction); verify(mBlurUtils).applyBlur(mViewRoot, (int) blurRadius, false); } + + @Test + public void testStartDreamEntryAnimationsOnAttachedNonLowLight() { + when(mStateController.isLowLightActive()).thenReturn(false); + + mController.onViewAttached(); + + verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView); + verify(mAnimationsController, never()).cancelRunningEntryAnimations(); + } + + @Test + public void testNeverStartDreamEntryAnimationsOnAttachedForLowLight() { + when(mStateController.isLowLightActive()).thenReturn(true); + + mController.onViewAttached(); + + verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView); + } + + @Test + public void testCancelDreamEntryAnimationsOnDetached() { + mController.onViewAttached(); + mController.onViewDetached(); + + verify(mAnimationsController).cancelRunningEntryAnimations(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index f370be190761..f04a37f4c3fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -253,6 +253,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED); verify(mStateController).setOverlayActive(false); verify(mStateController).setLowLightActive(false); + verify(mStateController).setEntryAnimationsFinished(false); } @Test @@ -273,24 +274,28 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Test public void testDecorViewNotAddedToWindowAfterDestroy() throws Exception { - when(mDreamOverlayContainerView.getParent()) - .thenReturn(mDreamOverlayContainerViewParent) - .thenReturn(null); - final IBinder proxy = mService.onBind(new Intent()); final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + // Destroy the service. + mService.onDestroy(); + mMainExecutor.runAllReady(); + // Inform the overlay service of dream starting. overlay.startDream(mWindowParams, mDreamOverlayCallback, DREAM_COMPONENT, false /*shouldShowComplication*/); + mMainExecutor.runAllReady(); - // Destroy the service. - mService.onDestroy(); + verify(mWindowManager, never()).addView(any(), any()); + } - // Run executor tasks. + @Test + public void testNeverRemoveDecorViewIfNotAdded() { + // Service destroyed before dream started. + mService.onDestroy(); mMainExecutor.runAllReady(); - verify(mWindowManager, never()).addView(any(), any()); + verify(mWindowManager, never()).removeView(any()); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java index d1d32a13589a..c21c7a2aacbe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -234,4 +234,20 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase { verify(mCallback, times(1)).onStateChanged(); assertThat(stateController.isLowLightActive()).isTrue(); } + + @Test + public void testNotifyEntryAnimationsFinishedChanged() { + final DreamOverlayStateController stateController = + new DreamOverlayStateController(mExecutor); + + stateController.addCallback(mCallback); + mExecutor.runAllReady(); + assertThat(stateController.areEntryAnimationsFinished()).isFalse(); + + stateController.setEntryAnimationsFinished(true); + mExecutor.runAllReady(); + + verify(mCallback, times(1)).onStateChanged(); + assertThat(stateController.areEntryAnimationsFinished()).isTrue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java index aa021781296a..85c28190d77b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java @@ -19,16 +19,20 @@ package com.android.systemui.dreams; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AlarmManager; +import android.content.Context; import android.content.res.Resources; import android.hardware.SensorPrivacyManager; import android.net.ConnectivityManager; @@ -55,6 +59,7 @@ 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.MockitoAnnotations; @@ -69,7 +74,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { "{count, plural, =1 {# notification} other {# notifications}}"; @Mock - DreamOverlayStatusBarView mView; + MockDreamOverlayStatusBarView mView; @Mock ConnectivityManager mConnectivityManager; @Mock @@ -105,6 +110,9 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { @Mock DreamOverlayStateController mDreamOverlayStateController; + @Captor + private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor; + private final Executor mMainExecutor = Runnable::run; DreamOverlayStatusBarViewController mController; @@ -115,6 +123,8 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { when(mResources.getString(R.string.dream_overlay_status_bar_notification_indicator)) .thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING); + doCallRealMethod().when(mView).setVisibility(anyInt()); + doCallRealMethod().when(mView).getVisibility(); mController = new DreamOverlayStatusBarViewController( mView, @@ -454,12 +464,10 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { public void testStatusBarHiddenWhenSystemStatusBarShown() { mController.onViewAttached(); - final ArgumentCaptor<StatusBarWindowStateListener> - callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class); - verify(mStatusBarWindowStateController).addListener(callbackCapture.capture()); - callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING); + updateEntryAnimationsFinished(); + updateStatusBarWindowState(true); - verify(mView).setVisibility(View.INVISIBLE); + assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE); } @Test @@ -467,29 +475,43 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { mController.onViewAttached(); reset(mView); - final ArgumentCaptor<StatusBarWindowStateListener> - callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class); - verify(mStatusBarWindowStateController).addListener(callbackCapture.capture()); - callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN); + updateEntryAnimationsFinished(); + updateStatusBarWindowState(false); - verify(mView).setVisibility(View.VISIBLE); + assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } @Test public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() { mController.onViewAttached(); + updateEntryAnimationsFinished(); mController.onViewDetached(); reset(mView); - final ArgumentCaptor<StatusBarWindowStateListener> - callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class); - verify(mStatusBarWindowStateController).addListener(callbackCapture.capture()); - callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING); + updateStatusBarWindowState(true); verify(mView, never()).setVisibility(anyInt()); } @Test + public void testNoChangeToVisibilityBeforeDreamStartedWhenStatusBarHidden() { + mController.onViewAttached(); + + // Trigger status bar window state change. + final StatusBarWindowStateListener listener = updateStatusBarWindowState(false); + + // Verify no visibility change because dream not started. + verify(mView, never()).setVisibility(anyInt()); + + // Dream entry animations finished. + updateEntryAnimationsFinished(); + + // Trigger another status bar window state change, and verify visibility change. + listener.onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN); + assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test public void testExtraStatusBarItemSetWhenItemsChange() { mController.onViewAttached(); when(mStatusBarItem.getView()).thenReturn(mStatusBarItemView); @@ -507,16 +529,75 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { public void testLowLightHidesStatusBar() { when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true); mController.onViewAttached(); + updateEntryAnimationsFinished(); + + final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); + callbackCapture.getValue().onStateChanged(); - verify(mView).setVisibility(View.INVISIBLE); + assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE); reset(mView); when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false); + callbackCapture.getValue().onStateChanged(); + + assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void testNoChangeToVisibilityBeforeDreamStartedWhenLowLightStateChange() { + when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false); + mController.onViewAttached(); + + // No change to visibility because dream not fully started. + verify(mView, never()).setVisibility(anyInt()); + + // Dream entry animations finished. + updateEntryAnimationsFinished(); + + // Trigger state change and verify visibility changed. final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture = ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); callbackCapture.getValue().onStateChanged(); - verify(mView).setVisibility(View.VISIBLE); + assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); + } + + private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) { + when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show); + final ArgumentCaptor<StatusBarWindowStateListener> + callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class); + verify(mStatusBarWindowStateController).addListener(callbackCapture.capture()); + final StatusBarWindowStateListener listener = callbackCapture.getValue(); + listener.onStatusBarWindowStateChanged(show ? WINDOW_STATE_SHOWING : WINDOW_STATE_HIDDEN); + return listener; + } + + private void updateEntryAnimationsFinished() { + when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true); + + verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture()); + final DreamOverlayStateController.Callback callback = mCallbackCaptor.getValue(); + callback.onStateChanged(); + } + + private static class MockDreamOverlayStatusBarView extends DreamOverlayStatusBarView { + private int mVisibility = View.VISIBLE; + + private MockDreamOverlayStatusBarView(Context context) { + super(context); + } + + @Override + public void setVisibility(int visibility) { + mVisibility = visibility; + } + + @Override + public int getVisibility() { + return mVisibility; + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java index 3b9e398141ee..b477592f8fbc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.dreams.complication; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -29,16 +30,18 @@ import androidx.lifecycle.Observer; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import com.android.systemui.dreams.DreamOverlayStateController; 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.MockitoAnnotations; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; @SmallTest @@ -77,9 +80,20 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { @Mock ComplicationLayoutParams mComplicationLayoutParams; + @Mock + DreamOverlayStateController mDreamOverlayStateController; + + @Captor + private ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> mObserverCaptor; + + @Captor + private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor; + @Complication.Category static final int COMPLICATION_CATEGORY = Complication.CATEGORY_SYSTEM; + private ComplicationHostViewController mController; + @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -91,32 +105,28 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { when(mViewHolder.getCategory()).thenReturn(COMPLICATION_CATEGORY); when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams); when(mComplicationView.getParent()).thenReturn(mComplicationHostView); - } - /** - * Ensures the lifecycle of complications is properly handled. - */ - @Test - public void testViewModelObservation() { - final ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> observerArgumentCaptor = - ArgumentCaptor.forClass(Observer.class); - final ComplicationHostViewController controller = new ComplicationHostViewController( + mController = new ComplicationHostViewController( mComplicationHostView, mLayoutEngine, + mDreamOverlayStateController, mLifecycleOwner, mViewModel); - controller.init(); - - verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner), - observerArgumentCaptor.capture()); + mController.init(); + } + /** + * Ensures the lifecycle of complications is properly handled. + */ + @Test + public void testViewModelObservation() { final Observer<Collection<ComplicationViewModel>> observer = - observerArgumentCaptor.getValue(); + captureComplicationViewModelsObserver(); - // Add complication and ensure it is added to the view. + // Add a complication and ensure it is added to the view. final HashSet<ComplicationViewModel> complications = new HashSet<>( - Arrays.asList(mComplicationViewModel)); + Collections.singletonList(mComplicationViewModel)); observer.onChanged(complications); verify(mLayoutEngine).addComplication(eq(mComplicationId), eq(mComplicationView), @@ -127,4 +137,48 @@ public class ComplicationHostViewControllerTest extends SysuiTestCase { verify(mLayoutEngine).removeComplication(eq(mComplicationId)); } + + @Test + public void testNewComplicationsBeforeEntryAnimationsFinishSetToInvisible() { + final Observer<Collection<ComplicationViewModel>> observer = + captureComplicationViewModelsObserver(); + + // Add a complication before entry animations are finished. + final HashSet<ComplicationViewModel> complications = new HashSet<>( + Collections.singletonList(mComplicationViewModel)); + observer.onChanged(complications); + + // The complication view should be set to invisible. + verify(mComplicationView).setVisibility(View.INVISIBLE); + } + + @Test + public void testNewComplicationsAfterEntryAnimationsFinishNotSetToInvisible() { + final Observer<Collection<ComplicationViewModel>> observer = + captureComplicationViewModelsObserver(); + + // Dream entry animations finished. + when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true); + final DreamOverlayStateController.Callback stateCallback = captureOverlayStateCallback(); + stateCallback.onStateChanged(); + + // Add a complication after entry animations are finished. + final HashSet<ComplicationViewModel> complications = new HashSet<>( + Collections.singletonList(mComplicationViewModel)); + observer.onChanged(complications); + + // The complication view should not be set to invisible. + verify(mComplicationView, never()).setVisibility(View.INVISIBLE); + } + + private Observer<Collection<ComplicationViewModel>> captureComplicationViewModelsObserver() { + verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner), + mObserverCaptor.capture()); + return mObserverCaptor.getValue(); + } + + private DreamOverlayStateController.Callback captureOverlayStateCallback() { + verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture()); + return mCallbackCaptor.getValue(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java deleted file mode 100644 index 640e6dc0461d..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.systemui.keyguard; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.UserIdInt; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.os.Looper; -import android.os.UserHandle; -import android.os.UserManager; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.broadcast.BroadcastDispatcher; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * runtest systemui -c com.android.systemui.keyguard.WorkLockActivityTest - */ -@SmallTest -@RunWith(AndroidJUnit4.class) -public class WorkLockActivityTest extends SysuiTestCase { - private static final @UserIdInt int USER_ID = 270; - private static final String CALLING_PACKAGE_NAME = "com.android.test"; - - private @Mock UserManager mUserManager; - private @Mock PackageManager mPackageManager; - private @Mock Context mContext; - private @Mock BroadcastDispatcher mBroadcastDispatcher; - private @Mock Drawable mDrawable; - private @Mock Drawable mBadgedDrawable; - - private WorkLockActivity mActivity; - - private static class WorkLockActivityTestable extends WorkLockActivity { - WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher, - UserManager userManager, PackageManager packageManager) { - super(broadcastDispatcher, userManager, packageManager); - attachBaseContext(baseContext); - } - } - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - if (Looper.myLooper() == null) { - Looper.prepare(); - } - mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher, mUserManager, - mPackageManager); - } - - @Test - public void testGetBadgedIcon() throws Exception { - ApplicationInfo info = new ApplicationInfo(); - when(mPackageManager.getApplicationInfoAsUser(eq(CALLING_PACKAGE_NAME), any(), - eq(USER_ID))).thenReturn(info); - when(mPackageManager.getApplicationIcon(eq(info))).thenReturn(mDrawable); - when(mUserManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID)))).thenReturn( - mBadgedDrawable); - mActivity.setIntent(new Intent() - .putExtra(Intent.EXTRA_USER_ID, USER_ID) - .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME)); - - assertEquals(mBadgedDrawable, mActivity.getBadgedIcon()); - } - - @Test - public void testUnregisteredFromDispatcher() { - mActivity.unregisterBroadcastReceiver(); - verify(mBroadcastDispatcher).unregisterReceiver(any()); - verify(mContext, never()).unregisterReceiver(any()); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt new file mode 100644 index 000000000000..c7f1decac670 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package com.android.systemui.keyguard + +import android.annotation.UserIdInt +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.graphics.drawable.Drawable +import android.os.Looper +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** runtest systemui -c com.android.systemui.keyguard.WorkLockActivityTest */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class WorkLockActivityTest : SysuiTestCase() { + private val context: Context = mock() + private val userManager: UserManager = mock() + private val packageManager: PackageManager = mock() + private val broadcastDispatcher: BroadcastDispatcher = mock() + private val drawable: Drawable = mock() + private val badgedDrawable: Drawable = mock() + private lateinit var activity: WorkLockActivity + + private class WorkLockActivityTestable + constructor( + baseContext: Context, + broadcastDispatcher: BroadcastDispatcher, + userManager: UserManager, + packageManager: PackageManager, + ) : WorkLockActivity(broadcastDispatcher, userManager, packageManager) { + init { + attachBaseContext(baseContext) + } + } + + @Before + fun setUp() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + activity = + WorkLockActivityTestable( + baseContext = context, + broadcastDispatcher = broadcastDispatcher, + userManager = userManager, + packageManager = packageManager + ) + } + + @Test + fun testGetBadgedIcon() { + val info = ApplicationInfo() + whenever( + packageManager.getApplicationInfoAsUser( + eq(CALLING_PACKAGE_NAME), + any<ApplicationInfoFlags>(), + eq(USER_ID) + ) + ) + .thenReturn(info) + whenever(packageManager.getApplicationIcon(ArgumentMatchers.eq(info))).thenReturn(drawable) + whenever(userManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID)))) + .thenReturn(badgedDrawable) + activity.intent = + Intent() + .putExtra(Intent.EXTRA_USER_ID, USER_ID) + .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME) + assertEquals(badgedDrawable, activity.badgedIcon) + } + + @Test + fun testUnregisteredFromDispatcher() { + activity.unregisterBroadcastReceiver() + verify(broadcastDispatcher).unregisterReceiver(any()) + verify(context, never()).unregisterReceiver(nullable()) + } + + @Test + fun onBackPressed_finishActivity() { + assertFalse(activity.isFinishing) + + activity.onBackPressed() + + assertFalse(activity.isFinishing) + } + + companion object { + @UserIdInt private val USER_ID = 270 + private const val CALLING_PACKAGE_NAME = "com.android.test" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index f18acbad14f2..0fb181d41484 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -23,14 +23,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.yield -/** - * Fake implementation of a quick affordance data source. - * - * This class is abstract to force tests to provide extensions of it as the system that references - * these configs uses each implementation's class type to refer to them. - */ -abstract class FakeKeyguardQuickAffordanceConfig( +/** Fake implementation of a quick affordance data source. */ +class FakeKeyguardQuickAffordanceConfig( override val key: String, + override val pickerName: String = key, + override val pickerIconResourceId: Int = 0, ) : KeyguardQuickAffordanceConfig { var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt new file mode 100644 index 000000000000..d2422ad7b53b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardQuickAffordanceSelectionManager + + @Before + fun setUp() { + underTest = KeyguardQuickAffordanceSelectionManager() + } + + @Test + fun setSelections() = + runBlocking(IMMEDIATE) { + var affordanceIdsBySlotId: Map<String, List<String>>? = null + val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this) + val slotId1 = "slot1" + val slotId2 = "slot2" + val affordanceId1 = "affordance1" + val affordanceId2 = "affordance2" + val affordanceId3 = "affordance3" + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1), + ), + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(affordanceId2), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId1, affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId1, affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId1, + affordanceIds = listOf(affordanceId3), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(affordanceId2), + ) + ) + + underTest.setSelections( + slotId = slotId2, + affordanceIds = listOf(), + ) + assertSelections( + affordanceIdsBySlotId, + mapOf( + slotId1 to listOf(affordanceId3), + slotId2 to listOf(), + ) + ) + + job.cancel() + } + + private suspend fun assertSelections( + observed: Map<String, List<String>>?, + expected: Map<String, List<String>>, + ) { + assertThat(underTest.getSelections()).isEqualTo(expected) + assertThat(observed).isEqualTo(expected) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 61a3f9f07600..2bd8e9aabab3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -50,7 +50,7 @@ class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(controller.intent).thenReturn(INTENT_1) - underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller) + underTest = QrCodeScannerKeyguardQuickAffordanceConfig(mock(), controller) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index c05beef6d624..5178154bdeee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -59,6 +59,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { underTest = QuickAccessWalletKeyguardQuickAffordanceConfig( + mock(), walletController, activityStarter, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt new file mode 100644 index 000000000000..5a7f2bb5cb37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation +import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardQuickAffordanceRepository + + private lateinit var config1: FakeKeyguardQuickAffordanceConfig + private lateinit var config2: FakeKeyguardQuickAffordanceConfig + + @Before + fun setUp() { + config1 = FakeKeyguardQuickAffordanceConfig("built_in:1") + config2 = FakeKeyguardQuickAffordanceConfig("built_in:2") + underTest = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(config1, config2), + ) + } + + @Test + fun setSelections() = + runBlocking(IMMEDIATE) { + var configsBySlotId: Map<String, List<KeyguardQuickAffordanceConfig>>? = null + val job = underTest.selections.onEach { configsBySlotId = it }.launchIn(this) + val slotId1 = "slot1" + val slotId2 = "slot2" + + underTest.setSelections(slotId1, listOf(config1.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to listOf(config1), + ), + ) + + underTest.setSelections(slotId2, listOf(config2.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to listOf(config1), + slotId2 to listOf(config2), + ), + ) + + underTest.setSelections(slotId1, emptyList()) + underTest.setSelections(slotId2, listOf(config1.key)) + assertSelections( + configsBySlotId, + mapOf( + slotId1 to emptyList(), + slotId2 to listOf(config1), + ), + ) + + job.cancel() + } + + @Test + fun getAffordancePickerRepresentations() { + assertThat(underTest.getAffordancePickerRepresentations()) + .isEqualTo( + listOf( + KeyguardQuickAffordancePickerRepresentation( + id = config1.key, + name = config1.pickerName, + iconResourceId = config1.pickerIconResourceId, + ), + KeyguardQuickAffordancePickerRepresentation( + id = config2.key, + name = config2.pickerName, + iconResourceId = config2.pickerIconResourceId, + ), + ) + ) + } + + @Test + fun getSlotPickerRepresentations() { + assertThat(underTest.getSlotPickerRepresentations()) + .isEqualTo( + listOf( + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + maxSelectedAffordances = 1, + ), + KeyguardSlotPickerRepresentation( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + maxSelectedAffordances = 1, + ), + ) + ) + } + + private suspend fun assertSelections( + observed: Map<String, List<KeyguardQuickAffordanceConfig>>?, + expected: Map<String, List<KeyguardQuickAffordanceConfig>>, + ) { + assertThat(observed).isEqualTo(expected) + assertThat(underTest.getSelections()) + .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } }) + expected.forEach { (slotId, configs) -> + assertThat(underTest.getSelections(slotId)).isEqualTo(configs) + } + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index b9ab9d37a805..53d9b87b2346 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -21,8 +21,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Position import com.android.systemui.doze.DozeHost import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever @@ -46,6 +48,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var dozeHost: DozeHost @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle + @Mock private lateinit var biometricUnlockController: BiometricUnlockController private lateinit var underTest: KeyguardRepositoryImpl @@ -59,6 +62,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, dozeHost, wakefulnessLifecycle, + biometricUnlockController, ) } @@ -190,7 +194,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun wakefullness() = runBlockingTest { + fun wakefulness() = runBlockingTest { val values = mutableListOf<WakefulnessModel>() val job = underTest.wakefulnessState.onEach(values::add).launchIn(this) @@ -217,4 +221,63 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { job.cancel() verify(wakefulnessLifecycle).removeObserver(captor.value) } + + @Test + fun isBouncerShowing() = runBlockingTest { + whenever(keyguardStateController.isBouncerShowing).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isBouncerShowing.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardStateController.Callback>() + verify(keyguardStateController).addCallback(captor.capture()) + + whenever(keyguardStateController.isBouncerShowing).thenReturn(true) + captor.value.onBouncerShowingChanged() + assertThat(latest).isTrue() + + whenever(keyguardStateController.isBouncerShowing).thenReturn(false) + captor.value.onBouncerShowingChanged() + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun biometricUnlockState() = runBlockingTest { + val values = mutableListOf<BiometricUnlockModel>() + val job = underTest.biometricUnlockState.onEach(values::add).launchIn(this) + + val captor = argumentCaptor<BiometricUnlockController.BiometricModeListener>() + verify(biometricUnlockController).addBiometricModeListener(captor.capture()) + + captor.value.onModeChanged(BiometricUnlockController.MODE_NONE) + captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK) + captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING) + captor.value.onModeChanged(BiometricUnlockController.MODE_SHOW_BOUNCER) + captor.value.onModeChanged(BiometricUnlockController.MODE_ONLY_WAKE) + captor.value.onModeChanged(BiometricUnlockController.MODE_UNLOCK_COLLAPSING) + captor.value.onModeChanged(BiometricUnlockController.MODE_DISMISS_BOUNCER) + captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM) + + assertThat(values) + .isEqualTo( + listOf( + // Initial value will be NONE, followed by onModeChanged() call + BiometricUnlockModel.NONE, + BiometricUnlockModel.NONE, + BiometricUnlockModel.WAKE_AND_UNLOCK, + BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING, + BiometricUnlockModel.SHOW_BOUNCER, + BiometricUnlockModel.ONLY_WAKE, + BiometricUnlockModel.UNLOCK_COLLAPSING, + BiometricUnlockModel.DISMISS_BOUNCER, + BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM, + ) + ) + + job.cancel() + verify(biometricUnlockController).removeBiometricModeListener(captor.value) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt index 64913c7c28c2..27d5d0a98978 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -128,11 +128,15 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { assertThat(steps.size).isEqualTo(3) assertThat(steps[0]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) assertThat(steps[1]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING, OWNER_NAME) + ) assertThat(steps[2]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME) + ) job.cancel() } @@ -174,15 +178,22 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() { } private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) { - assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME)) fractions.forEachIndexed { index, fraction -> assertThat(steps[index + 1]) .isEqualTo( - TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + TransitionStep( + AOD, + LOCKSCREEN, + fraction.toFloat(), + TransitionState.RUNNING, + OWNER_NAME + ) ) } assertThat(steps[steps.size - 1]) - .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME)) assertThat(wtfHandler.failed).isFalse() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 7116cc101d3f..8b6603d79244 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -25,10 +25,14 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter @@ -37,6 +41,8 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runBlockingTest import org.junit.Before import org.junit.Test @@ -189,6 +195,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { /* startActivity= */ true, ), ) + + private val IMMEDIATE = Dispatchers.Main.immediate } @Mock private lateinit var lockPatternUtils: LockPatternUtils @@ -213,10 +221,20 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { whenever(expandable.activityLaunchController()).thenReturn(animationController) homeControls = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) + val quickAccessWallet = + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) + val qrCodeScanner = + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), + ) underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), @@ -229,14 +247,8 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ), KeyguardQuickAffordancePosition.BOTTOM_END to listOf( - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {}, - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {}, + quickAccessWallet, + qrCodeScanner, ), ), ), @@ -244,6 +256,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + }, + repository = { quickAffordanceRepository }, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index ae32ba6676be..33645354d9f3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -22,22 +22,31 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.yield import org.junit.Before @@ -47,6 +56,7 @@ import org.junit.runners.JUnit4 import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @@ -62,6 +72,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig + private lateinit var featureFlags: FakeFeatureFlags @Before fun setUp() { @@ -71,20 +82,25 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { repository.setKeyguardShowing(true) homeControls = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) quickAccessWallet = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {} + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) qrCodeScanner = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) + + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = setOf(homeControls, quickAccessWallet, qrCodeScanner), + ) + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + } underTest = KeyguardQuickAffordanceInteractor( @@ -107,6 +123,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = featureFlags, + repository = { quickAffordanceRepository }, ) } @@ -210,6 +228,270 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun select() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + var startConfig: KeyguardQuickAffordanceModel? = null + val job1 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { startConfig = it } + .launchIn(this) + var endConfig: KeyguardQuickAffordanceModel? = null + val job2 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { endConfig = it } + .launchIn(this) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${homeControls.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(homeControls.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + underTest.select( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + quickAccessWallet.key + ) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(quickAccessWallet.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key) + yield() + yield() + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + + "::${qrCodeScanner.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to + listOf(quickAccessWallet.key), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(qrCodeScanner.key), + ) + ) + + job1.cancel() + job2.cancel() + } + + @Test + fun `unselect - one`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + var startConfig: KeyguardQuickAffordanceModel? = null + val job1 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + .onEach { startConfig = it } + .launchIn(this) + var endConfig: KeyguardQuickAffordanceModel? = null + val job2 = + underTest + .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + .onEach { endConfig = it } + .launchIn(this) + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) + yield() + yield() + + underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Visible( + configKey = + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + + "::${quickAccessWallet.key}", + icon = ICON, + activationState = ActivationState.NotSupported, + ) + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(quickAccessWallet.key), + ) + ) + + underTest.unselect( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + quickAccessWallet.key + ) + yield() + yield() + + assertThat(startConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(endConfig) + .isEqualTo( + KeyguardQuickAffordanceModel.Hidden, + ) + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + + job1.cancel() + job2.cancel() + } + + @Test + fun `unselect - all`() = + runBlocking(IMMEDIATE) { + featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + qrCodeScanner.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) + ) + + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) + yield() + yield() + underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) + yield() + yield() + + underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null) + yield() + yield() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to + listOf(quickAccessWallet.key), + ) + ) + + underTest.unselect( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + null, + ) + yield() + yield() + + assertThat(underTest.getSelections()) + .isEqualTo( + mapOf<String, List<String>>( + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(), + KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), + ) + ) + } + companion object { private val ICON: Icon = mock { whenever(this.contentDescription) @@ -220,5 +502,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 0424c28a1c24..6333b244fd38 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -54,9 +54,11 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { @Test fun `transition collectors receives only appropriate events`() = runBlocking(IMMEDIATE) { - var goneToAodSteps = mutableListOf<TransitionStep>() + var lockscreenToAodSteps = mutableListOf<TransitionStep>() val job1 = - underTest.goneToAodTransition.onEach { goneToAodSteps.add(it) }.launchIn(this) + underTest.lockscreenToAodTransition + .onEach { lockscreenToAodSteps.add(it) } + .launchIn(this) var aodToLockscreenSteps = mutableListOf<TransitionStep>() val job2 = @@ -70,14 +72,14 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) - steps.add(TransitionStep(GONE, AOD, 0f, STARTED)) - steps.add(TransitionStep(GONE, AOD, 0.1f, RUNNING)) - steps.add(TransitionStep(GONE, AOD, 0.2f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING)) steps.forEach { repository.sendTransitionStep(it) } assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5)) - assertThat(goneToAodSteps).isEqualTo(steps.subList(5, 8)) + assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8)) job1.cancel() job2.cancel() @@ -119,10 +121,8 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { fun keyguardStateTests() = runBlocking(IMMEDIATE) { var finishedSteps = mutableListOf<KeyguardState>() - val job1 = + val job = underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this) - var startedSteps = mutableListOf<KeyguardState>() - val job2 = underTest.startedKeyguardState.onEach { startedSteps.add(it) }.launchIn(this) val steps = mutableListOf<TransitionStep>() @@ -137,10 +137,60 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { steps.forEach { repository.sendTransitionStep(it) } assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD)) - assertThat(startedSteps).isEqualTo(listOf(LOCKSCREEN, AOD, GONE)) - job1.cancel() - job2.cancel() + job.cancel() + } + + @Test + fun finishedKeyguardTransitionStepTests() = + runBlocking(IMMEDIATE) { + var finishedSteps = mutableListOf<TransitionStep>() + val job = + underTest.finishedKeyguardTransitionStep + .onEach { finishedSteps.add(it) } + .launchIn(this) + + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { repository.sendTransitionStep(it) } + + assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5])) + + job.cancel() + } + + @Test + fun startedKeyguardTransitionStepTests() = + runBlocking(IMMEDIATE) { + var startedSteps = mutableListOf<TransitionStep>() + val job = + underTest.startedKeyguardTransitionStep + .onEach { startedSteps.add(it) } + .launchIn(this) + + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { repository.sendTransitionStep(it) } + + assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6])) + + job.cancel() } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index f73d1ecf9373..78148c4d3d1b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -23,10 +23,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor @@ -41,6 +45,8 @@ import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runBlockingTest @@ -82,20 +88,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { .thenReturn(RETURNED_BURN_IN_OFFSET) homeControlsQuickAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS) quickAccessWalletAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) {} + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) qrCodeScannerAffordanceConfig = - object : - FakeKeyguardQuickAffordanceConfig( - BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER - ) {} + FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) registry = FakeKeyguardQuickAffordanceRegistry( mapOf( @@ -116,6 +115,18 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) + val quickAffordanceRepository = + KeyguardQuickAffordanceRepository( + scope = CoroutineScope(IMMEDIATE), + backgroundDispatcher = IMMEDIATE, + selectionManager = KeyguardQuickAffordanceSelectionManager(), + configs = + setOf( + homeControlsQuickAffordanceConfig, + quickAccessWalletAffordanceConfig, + qrCodeScannerAffordanceConfig, + ), + ) underTest = KeyguardBottomAreaViewModel( keyguardInteractor = keyguardInteractor, @@ -127,6 +138,11 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { keyguardStateController = keyguardStateController, userTracker = userTracker, activityStarter = activityStarter, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false) + }, + repository = { quickAffordanceRepository }, ), bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository), burnInHelperWrapper = burnInHelperWrapper, @@ -576,5 +592,6 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { companion object { private const val DEFAULT_BURN_IN_OFFSET = 5 private const val RETURNED_BURN_IN_OFFSET = 3 + private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index f96c39f007dd..aa1114b8736e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider import com.android.systemui.statusbar.notification.collection.render.NodeController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider @@ -86,6 +87,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { private val mRemoteInputManager: NotificationRemoteInputManager = mock() private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock() private val mHeaderController: NodeController = mock() + private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock() private lateinit var mEntry: NotificationEntry private lateinit var mGroupSummary: NotificationEntry @@ -110,6 +112,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { mHeadsUpViewBinder, mNotificationInterruptStateProvider, mRemoteInputManager, + mLaunchFullScreenIntentProvider, mHeaderController, mExecutor) mCoordinator.attach(mNotifPipeline) @@ -242,6 +245,20 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + fun testOnEntryAdded_shouldFullScreen() { + setShouldFullScreen(mEntry) + mCollectionListener.onEntryAdded(mEntry) + verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry) + } + + @Test + fun testOnEntryAdded_shouldNotFullScreen() { + setShouldFullScreen(mEntry, should = false) + mCollectionListener.onEntryAdded(mEntry) + verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any()) + } + + @Test fun testPromotesAddedHUN() { // GIVEN the current entry should heads up whenever(mNotificationInterruptStateProvider.shouldHeadsUp(mEntry)).thenReturn(true) @@ -794,6 +811,11 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { .thenReturn(should) } + private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) { + whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) + .thenReturn(should) + } + private fun finishBind(entry: NotificationEntry) { verify(mHeadsUpManager, never()).showNotification(entry) withArgCaptor<BindCallback> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 6fa217415044..b17747ad2d92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -143,7 +143,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController, mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); - mBiometricUnlockController.setBiometricModeListener(mBiometricModeListener); + mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index 9c56c2670c63..6fb68938b00d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -45,7 +45,7 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; -import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; +import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter; import com.android.systemui.utils.leaks.LeakCheckedTest; import org.junit.Before; @@ -80,7 +80,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { layout, StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), - mock(WifiViewModel.class), + mock(WifiUiAdapter.class), mock(MobileUiAdapter.class), mMobileContextProvider, mock(DarkIconDispatcher.class)); @@ -124,14 +124,14 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { LinearLayout group, StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, - WifiViewModel wifiViewModel, + WifiUiAdapter wifiUiAdapter, MobileUiAdapter mobileUiAdapter, MobileContextProvider contextProvider, DarkIconDispatcher darkIconDispatcher) { super(group, location, statusBarPipelineFlags, - wifiViewModel, + wifiUiAdapter, mobileUiAdapter, contextProvider, darkIconDispatcher); @@ -172,7 +172,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { super(group, StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), - mock(WifiViewModel.class), + mock(WifiUiAdapter.class), mock(MobileUiAdapter.class), contextProvider); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index c4098578197b..ce54d784520c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -67,8 +67,8 @@ import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -123,8 +123,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { @Mock private ShadeControllerImpl mShadeController; @Mock - private NotifPipeline mNotifPipeline; - @Mock private NotificationVisibilityProvider mVisibilityProvider; @Mock private ActivityIntentHelper mActivityIntentHelper; @@ -197,7 +195,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { getContext(), mHandler, mUiBgExecutor, - mNotifPipeline, mVisibilityProvider, headsUpManager, mActivityStarter, @@ -222,7 +219,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mock(NotificationPresenter.class), mock(NotificationPanelViewController.class), mActivityLaunchAnimator, - notificationAnimationProvider + notificationAnimationProvider, + mock(LaunchFullScreenIntentProvider.class) ); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg @@ -384,11 +382,9 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { NotificationEntry entry = mock(NotificationEntry.class); when(entry.getImportance()).thenReturn(NotificationManager.IMPORTANCE_HIGH); when(entry.getSbn()).thenReturn(sbn); - when(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(eq(entry))) - .thenReturn(true); // WHEN - mNotificationActivityStarter.handleFullScreenIntent(entry); + mNotificationActivityStarter.launchFullScreenIntent(entry); // THEN display should try wake up for the full screen intent verify(mCentralSurfaces).wakeUpForFullScreenIntent(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index c5841098010a..37457b308597 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.lifecycle.InstantTaskExecutorRule import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON -import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor @@ -40,6 +39,7 @@ import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope @@ -70,7 +70,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor - private lateinit var viewModel: WifiViewModel + private lateinit var viewModel: LocationBasedWifiViewModel private lateinit var scope: CoroutineScope private lateinit var airplaneModeViewModel: AirplaneModeViewModel @@ -105,23 +105,19 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { scope, statusBarPipelineFlags, wifiConstants, - ) + ).home } @Test fun constructAndBind_hasCorrectSlot() { - val view = ModernStatusBarWifiView.constructAndBind( - context, "slotName", viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, "slotName", viewModel) assertThat(view.slot).isEqualTo("slotName") } @Test fun getVisibleState_icon_returnsIcon() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_ICON, /* animate= */ false) @@ -130,9 +126,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { @Test fun getVisibleState_dot_returnsDot() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_DOT, /* animate= */ false) @@ -141,9 +135,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { @Test fun getVisibleState_hidden_returnsHidden() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_HIDDEN, /* animate= */ false) @@ -155,9 +147,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { @Test fun setVisibleState_icon_iconShownDotHidden() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_ICON, /* animate= */ false) @@ -172,9 +162,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { @Test fun setVisibleState_dot_iconHiddenDotShown() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_DOT, /* animate= */ false) @@ -189,9 +177,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { @Test fun setVisibleState_hidden_iconAndDotHidden() { - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) view.setVisibleState(STATE_HIDDEN, /* animate= */ false) @@ -211,9 +197,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2) ) - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -230,9 +214,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2) ) - val view = ModernStatusBarWifiView.constructAndBind( - context, SLOT_NAME, viewModel, StatusBarLocation.HOME - ) + val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) ViewUtils.attachView(view) testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java index 4f1fb02ecdcd..26df03f31b9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -25,6 +26,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.net.TetheringManager; @@ -36,6 +38,7 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; +import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; @@ -96,6 +99,9 @@ public class HotspotControllerImplTest extends SysuiTestCase { }).when(mWifiManager).registerSoftApCallback(any(Executor.class), any(WifiManager.SoftApCallback.class)); + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_show_wifi_tethering, true); + Handler handler = new Handler(mLooper.getLooper()); mController = new HotspotControllerImpl(mContext, handler, handler, mDumpManager); @@ -176,4 +182,18 @@ public class HotspotControllerImplTest extends SysuiTestCase { verify(mCallback1).onHotspotAvailabilityChanged(false); } + + @Test + public void testHotspotSupported_resource_false() { + mContext.getOrCreateTestableResources() + .addOverride(R.bool.config_show_wifi_tethering, false); + + Handler handler = new Handler(mLooper.getLooper()); + + HotspotController controller = + new HotspotControllerImpl(mContext, handler, handler, mDumpManager); + + verifyNoMoreInteractions(mTetheringManager); + assertFalse(controller.isHotspotSupported()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt index 120bf791c462..b485693af50f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -23,6 +23,8 @@ import android.os.UserHandle import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.user.data.repository.FakeUserRepository @@ -55,6 +57,8 @@ class GuestUserInteractorTest : SysuiTestCase() { @Mock private lateinit var dismissDialog: () -> Unit @Mock private lateinit var selectUser: (Int) -> Unit @Mock private lateinit var switchUser: (Int) -> Unit + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver private lateinit var underTest: GuestUserInteractor @@ -87,10 +91,18 @@ class GuestUserInteractorTest : SysuiTestCase() { repository = repository, ), uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, ) } @Test + fun `registers broadcast receivers`() { + verify(resumeSessionReceiver).register() + verify(resetOrExitSessionReceiver).register() + } + + @Test fun `onDeviceBootCompleted - allowed to add - create guest`() = runBlocking(IMMEDIATE) { setAllowedToAdd() diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index 1680c36cef87..58f55314c1b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -21,6 +21,8 @@ import android.app.ActivityManager import android.app.admin.DevicePolicyManager import android.os.UserManager import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags @@ -48,6 +50,8 @@ abstract class UserInteractorTest : SysuiTestCase() { @Mock protected lateinit var devicePolicyManager: DevicePolicyManager @Mock protected lateinit var uiEventLogger: UiEventLogger @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver protected lateinit var underTest: UserInteractor @@ -107,6 +111,8 @@ abstract class UserInteractorTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, refreshUsersScheduler = refreshUsersScheduler, uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, ) ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index c12a868dbaed..116023aca655 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -23,6 +23,8 @@ import android.graphics.drawable.Drawable import android.os.UserManager import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger +import com.android.systemui.GuestResetOrExitSessionReceiver +import com.android.systemui.GuestResumeSessionReceiver import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text import com.android.systemui.flags.FakeFeatureFlags @@ -69,6 +71,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver + @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver private lateinit var underTest: UserSwitcherViewModel @@ -104,6 +108,8 @@ class UserSwitcherViewModelTest : SysuiTestCase() { devicePolicyManager = devicePolicyManager, refreshUsersScheduler = refreshUsersScheduler, uiEventLogger = uiEventLogger, + resumeSessionReceiver = resumeSessionReceiver, + resetOrExitSessionReceiver = resetOrExitSessionReceiver, ) underTest = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 11178db8afd2..627bd096143e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.common.shared.model.Position +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel import kotlinx.coroutines.flow.Flow @@ -52,6 +53,12 @@ class FakeKeyguardRepository : KeyguardRepository { private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP) override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState + private val _isBouncerShowing = MutableStateFlow(false) + override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing + + private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE) + override val biometricUnlockState: Flow<BiometricUnlockModel> = _biometricUnlockState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java index 23c7a6139de8..2d6d29a50a74 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java @@ -62,7 +62,11 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager> } @Override - public void setSignalIcon(String slot, WifiIconState state) { + public void setWifiIcon(String slot, WifiIconState state) { + } + + @Override + public void setNewWifiIcon() { } @Override diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 252dcfc5eb9e..4430bb4b3292 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -26,6 +26,7 @@ import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; @@ -254,11 +255,13 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private boolean mSafeMode; private int mMaxWidgetBitmapMemory; private boolean mIsProviderInfoPersisted; + private boolean mIsCombinedBroadcastEnabled; AppWidgetServiceImpl(Context context) { mContext = context; } + @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public void onStart() { mPackageManager = AppGlobals.getPackageManager(); mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); @@ -277,6 +280,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku mIsProviderInfoPersisted = !ActivityManager.isLowRamDeviceStatic() && DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.PERSISTS_WIDGET_PROVIDER_INFO, true); + mIsCombinedBroadcastEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.COMBINED_BROADCAST_ENABLED, true); if (DEBUG_PROVIDER_INFO_CACHE && !mIsProviderInfoPersisted) { Slog.d(TAG, "App widget provider info will not be persisted on this device"); } @@ -1123,16 +1128,16 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku final int widgetCount = provider.widgets.size(); if (widgetCount == 1) { - // Tell the provider that it's ready. - sendEnableIntentLocked(provider); + // If we are binding the very first widget from a provider, we will send + // a combined broadcast or 2 separate broadcasts to tell the provider that + // it's ready, and we need them to provide the update now. + sendEnableAndUpdateIntentLocked(provider, new int[]{appWidgetId}); + } else { + // For any widget other then the first one, we just send update intent + // as we normally would. + sendUpdateIntentLocked(provider, new int[]{appWidgetId}); } - // Send an update now -- We need this update now, and just for this appWidgetId. - // It's less critical when the next one happens, so when we schedule the next one, - // we add updatePeriodMillis to its start time. That time will have some slop, - // but that's okay. - sendUpdateIntentLocked(provider, new int[] {appWidgetId}); - // Schedule the future updates. registerForBroadcastsLocked(provider, getWidgetIds(provider.widgets)); @@ -2361,6 +2366,22 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku cancelBroadcastsLocked(provider); } + private void sendEnableAndUpdateIntentLocked(@NonNull Provider p, int[] appWidgetIds) { + final boolean canSendCombinedBroadcast = mIsCombinedBroadcastEnabled && p.info != null + && p.info.isExtendedFromAppWidgetProvider; + if (!canSendCombinedBroadcast) { + // If this function is called by mistake, send two separate broadcasts instead + sendEnableIntentLocked(p); + sendUpdateIntentLocked(p, appWidgetIds); + return; + } + + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); + intent.setComponent(p.id.componentName); + sendBroadcastAsUser(intent, p.id.getProfile()); + } + private void sendEnableIntentLocked(Provider p) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED); intent.setComponent(p.id.componentName); @@ -2852,7 +2873,6 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (provider.widgets.size() > 0) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "appwidget init " + provider.id.componentName.getPackageName()); - sendEnableIntentLocked(provider); provider.widgets.forEach(widget -> { widget.trackingUpdate = true; Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, @@ -2861,7 +2881,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku Log.i(TAG, "Widget update scheduled on unlock " + widget.toString()); }); int[] appWidgetIds = getWidgetIds(provider.widgets); - sendUpdateIntentLocked(provider, appWidgetIds); + sendEnableAndUpdateIntentLocked(provider, appWidgetIds); registerForBroadcastsLocked(provider, appWidgetIds); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index df5113b16f49..85d6f293852c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2886,10 +2886,13 @@ public class ActivityManagerService extends IActivityManager.Stub || event == Event.ACTIVITY_DESTROYED)) { contentCaptureService.notifyActivityEvent(userId, activity, event); } - // TODO(b/201234353): Move the logic to client side. - if (mVoiceInteractionManagerProvider != null && (event == Event.ACTIVITY_PAUSED - || event == Event.ACTIVITY_RESUMED || event == Event.ACTIVITY_STOPPED)) { - mVoiceInteractionManagerProvider.notifyActivityEventChanged(); + // Currently we have move most of logic to the client side. When the activity lifecycle + // event changed, the client side will notify the VoiceInteractionManagerService. But + // when the application process died, the VoiceInteractionManagerService will miss the + // activity lifecycle event changed, so we still need ACTIVITY_DESTROYED event here to + // know if the activity has been destroyed. + if (mVoiceInteractionManagerProvider != null && event == Event.ACTIVITY_DESTROYED) { + mVoiceInteractionManagerProvider.notifyActivityDestroyed(appToken); } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index d2ba9c6dbfe4..53fcf3293525 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -41,6 +41,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityThread; import android.app.AlarmManager; import android.app.AppGlobals; import android.app.AppOpsManager; @@ -152,6 +153,7 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.os.VibratorManager; +import android.provider.DeviceConfig; import android.provider.Settings; import android.provider.Settings.System; import android.service.notification.ZenModeConfig; @@ -173,6 +175,7 @@ import android.widget.Toast; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.EventLogTags; @@ -231,6 +234,7 @@ public class AudioService extends IAudioService.Stub AudioSystemAdapter.OnVolRangeInitRequestListener { private static final String TAG = "AS.AudioService"; + private static final boolean CONFIG_DEFAULT_VAL = false; private final AudioSystemAdapter mAudioSystem; private final SystemServerAdapter mSystemServer; @@ -981,6 +985,7 @@ public class AudioService extends IAudioService.Stub * @param looper Looper to use for the service's message handler. If this is null, an * {@link AudioSystemThread} is created as the messaging thread instead. */ + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public AudioService(Context context, AudioSystemAdapter audioSystem, SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper, AppOpsManager appOps) { @@ -1020,8 +1025,12 @@ public class AudioService extends IAudioService.Stub mUseVolumeGroupAliases = mContext.getResources().getBoolean( com.android.internal.R.bool.config_handleVolumeAliasesUsingVolumeGroups); - mNotifAliasRing = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); + mNotifAliasRing = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false); + + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + ActivityThread.currentApplication().getMainExecutor(), + this::onDeviceConfigChange); // Initialize volume // Priority 1 - Android Property @@ -1240,6 +1249,22 @@ public class AudioService extends IAudioService.Stub } /** + * Separating notification volume from ring is NOT of aliasing the corresponding streams + * @param properties + */ + private void onDeviceConfigChange(DeviceConfig.Properties properties) { + Set<String> changeSet = properties.getKeyset(); + if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) { + boolean newNotifAliasRing = !properties.getBoolean( + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + if (mNotifAliasRing != newNotifAliasRing) { + mNotifAliasRing = newNotifAliasRing; + updateStreamVolumeAlias(true, TAG); + } + } + } + + /** * Called by handling of MSG_INIT_STREAMS_VOLUMES */ private void onInitStreamsAndVolumes() { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index ca4b7475f7dc..dbe971f32dc1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -100,9 +100,7 @@ class FaceAuthenticationClient extends AuthenticationClient<AidlSession> owner, cookie, requireConfirmation, sensorId, logger, biometricContext, isStrongBiometric, null /* taskStackListener */, lockoutCache, allowBackgroundAuthentication, - context.getResources().getBoolean( - com.android.internal.R.bool.system_server_plays_face_haptics) - /* shouldVibrate */, + false /* shouldVibrate */, isKeyguardBypassEnabled); setRequestId(requestId); mUsageStats = usageStats; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java index 9baca98e4d2b..91eec7d19b3a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java @@ -74,7 +74,7 @@ class FaceAuthenticationClient extends AuthenticationClient<IBiometricsFace> { super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted, owner, cookie, requireConfirmation, sensorId, logger, biometricContext, isStrongBiometric, null /* taskStackListener */, - lockoutTracker, allowBackgroundAuthentication, true /* shouldVibrate */, + lockoutTracker, allowBackgroundAuthentication, false /* shouldVibrate */, isKeyguardBypassEnabled); setRequestId(requestId); mUsageStats = usageStats; diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index a778b573f225..9aecf783a881 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -69,7 +69,6 @@ import java.util.function.Supplier; class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> implements Udfps, LockoutConsumer, PowerPressHandler { private static final String TAG = "FingerprintAuthenticationClient"; - private static final int MESSAGE_IGNORE_AUTH = 1; private static final int MESSAGE_AUTH_SUCCESS = 2; private static final int MESSAGE_FINGER_UP = 3; @NonNull @@ -136,7 +135,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> taskStackListener, lockoutCache, allowBackgroundAuthentication, - true /* shouldVibrate */, + false /* shouldVibrate */, false /* isKeyguardBypassEnabled */); setRequestId(requestId); mLockoutCache = lockoutCache; @@ -235,12 +234,6 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> () -> { long delay = 0; if (authenticated && mSensorProps.isAnySidefpsType()) { - if (mHandler.hasMessages(MESSAGE_IGNORE_AUTH)) { - Slog.i(TAG, "(sideFPS) Ignoring auth due to recent power press"); - onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, - true); - return; - } delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp; if (mSideFpsLastAcquireStartTime != -1) { @@ -497,16 +490,12 @@ class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession> if (mSensorProps.isAnySidefpsType()) { Slog.i(TAG, "(sideFPS): onPowerPressed"); mHandler.post(() -> { - if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) { - Slog.i(TAG, "(sideFPS): Ignoring auth in queue"); - mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); - // Do not call onError() as that will send an additional callback to coex. - onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true); - } - mHandler.removeMessages(MESSAGE_IGNORE_AUTH); - mHandler.postDelayed(() -> { - }, MESSAGE_IGNORE_AUTH, mIgnoreAuthFor); - + Slog.i(TAG, "(sideFPS): finishing auth"); + // Ignore auths after a power has been detected + mHandler.removeMessages(MESSAGE_AUTH_SUCCESS); + // Do not call onError() as that will send an additional callback to coex. + onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true); + mSensorOverlays.hide(getSensorId()); }); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 7ed1a514f9bc..0d620fd3a9e4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -81,7 +81,7 @@ class FingerprintAuthenticationClient extends AuthenticationClient<IBiometricsFi super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted, owner, cookie, requireConfirmation, sensorId, logger, biometricContext, isStrongBiometric, taskStackListener, lockoutTracker, allowBackgroundAuthentication, - true /* shouldVibrate */, false /* isKeyguardBypassEnabled */); + false /* shouldVibrate */, false /* isKeyguardBypassEnabled */); setRequestId(requestId); mLockoutFrameworkImpl = lockoutTracker; mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController); diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 5589673973c3..e4aa5e574955 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -157,7 +157,7 @@ public final class DreamManagerService extends SystemService { mDozeConfig = new AmbientDisplayConfiguration(mContext); mUiEventLogger = new UiEventLoggerImpl(); mDreamUiEventLogger = new DreamUiEventLoggerImpl( - mContext.getResources().getString(R.string.config_loggable_dream_prefix)); + mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes)); AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext); mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent()); mDreamsOnlyEnabledForSystemUser = diff --git a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java index 26ca74a8d808..96ebcbb6a771 100644 --- a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java +++ b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java @@ -26,10 +26,10 @@ import com.android.internal.util.FrameworkStatsLog; * @hide */ public class DreamUiEventLoggerImpl implements DreamUiEventLogger { - final String mLoggableDreamPrefix; + private final String[] mLoggableDreamPrefixes; - DreamUiEventLoggerImpl(String loggableDreamPrefix) { - mLoggableDreamPrefix = loggableDreamPrefix; + DreamUiEventLoggerImpl(String[] loggableDreamPrefixes) { + mLoggableDreamPrefixes = loggableDreamPrefixes; } @Override @@ -38,13 +38,20 @@ public class DreamUiEventLoggerImpl implements DreamUiEventLogger { if (eventID <= 0) { return; } - final boolean isFirstPartyDream = - mLoggableDreamPrefix.isEmpty() ? false : dreamComponentName.startsWith( - mLoggableDreamPrefix); FrameworkStatsLog.write(FrameworkStatsLog.DREAM_UI_EVENT_REPORTED, /* uid = 1 */ 0, /* event_id = 2 */ eventID, /* instance_id = 3 */ 0, - /* dream_component_name = 4 */ isFirstPartyDream ? dreamComponentName : "other"); + /* dream_component_name = 4 */ + isFirstPartyDream(dreamComponentName) ? dreamComponentName : "other"); + } + + private boolean isFirstPartyDream(String dreamComponentName) { + for (int i = 0; i < mLoggableDreamPrefixes.length; ++i) { + if (dreamComponentName.startsWith(mLoggableDreamPrefixes[i])) { + return true; + } + } + return false; } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 2eb2cf643c42..ccab96888e8f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -496,7 +496,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** The most recently given options. */ private ActivityOptions mPendingOptions; /** Non-null if {@link #mPendingOptions} specifies the remote animation. */ - private RemoteAnimationAdapter mPendingRemoteAnimation; + RemoteAnimationAdapter mPendingRemoteAnimation; private RemoteTransition mPendingRemoteTransition; ActivityOptions returningOptions; // options that are coming back via convertToTranslucent AppTimeTracker appTimeTracker; // set if we are tracking the time in this app/task/activity @@ -812,7 +812,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A StartingData mStartingData; WindowState mStartingWindow; StartingSurfaceController.StartingSurface mStartingSurface; - boolean startingDisplayed; boolean startingMoved; /** The last set {@link DropInputMode} for this activity surface. */ @@ -821,13 +820,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** Whether the input to this activity will be dropped during the current playing animation. */ private boolean mIsInputDroppedForAnimation; - /** - * If it is non-null, it requires all activities who have the same starting data to be drawn - * to remove the starting window. - * TODO(b/189385912): Remove starting window related fields after migrating them to task. - */ - private StartingData mSharedStartingData; - boolean mHandleExitSplashScreen; @TransferSplashScreenState int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE; @@ -1200,14 +1192,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A pw.print(" firstWindowDrawn="); pw.print(firstWindowDrawn); pw.print(" mIsExiting="); pw.println(mIsExiting); } - if (mSharedStartingData != null) { - pw.println(prefix + "mSharedStartingData=" + mSharedStartingData); - } - if (mStartingWindow != null || mStartingSurface != null - || startingDisplayed || startingMoved || mVisibleSetFromTransferredStartingWindow) { + if (mStartingWindow != null || mStartingData != null || mStartingSurface != null + || startingMoved || mVisibleSetFromTransferredStartingWindow) { pw.print(prefix); pw.print("startingWindow="); pw.print(mStartingWindow); pw.print(" startingSurface="); pw.print(mStartingSurface); - pw.print(" startingDisplayed="); pw.print(startingDisplayed); + pw.print(" startingDisplayed="); pw.print(isStartingWindowDisplayed()); pw.print(" startingMoved="); pw.print(startingMoved); pw.println(" mVisibleSetFromTransferredStartingWindow=" + mVisibleSetFromTransferredStartingWindow); @@ -2690,13 +2679,23 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + boolean isStartingWindowDisplayed() { + final StartingData data = mStartingData != null ? mStartingData : task != null + ? task.mSharedStartingData : null; + return data != null && data.mIsDisplayed; + } + /** Called when the starting window is added to this activity. */ void attachStartingWindow(@NonNull WindowState startingWindow) { startingWindow.mStartingData = mStartingData; mStartingWindow = startingWindow; - // The snapshot type may have called associateStartingDataWithTask(). - if (mStartingData != null && mStartingData.mAssociatedTask != null) { - attachStartingSurfaceToAssociatedTask(); + if (mStartingData != null) { + if (mStartingData.mAssociatedTask != null) { + // The snapshot type may have called associateStartingDataWithTask(). + attachStartingSurfaceToAssociatedTask(); + } else if (isEmbedded()) { + associateStartingWindowWithTaskIfNeeded(); + } } } @@ -2711,11 +2710,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** Called when the starting window is not added yet but its data is known to fill the task. */ private void associateStartingDataWithTask() { mStartingData.mAssociatedTask = task; - task.forAllActivities(r -> { - if (r.mVisibleRequested && !r.firstWindowDrawn) { - r.mSharedStartingData = mStartingData; - } - }); + task.mSharedStartingData = mStartingData; } /** Associates and attaches an added starting window to the current task. */ @@ -2746,10 +2741,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void removeStartingWindowAnimation(boolean prepareAnimation) { mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE; - if (mSharedStartingData != null) { - mSharedStartingData.mAssociatedTask.forAllActivities(r -> { - r.mSharedStartingData = null; - }); + if (task != null) { + task.mSharedStartingData = null; } if (mStartingWindow == null) { if (mStartingData != null) { @@ -2772,7 +2765,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mStartingData = null; mStartingSurface = null; mStartingWindow = null; - startingDisplayed = false; if (surface == null) { ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "startingWindow was set but " + "startingSurface==null, couldn't remove"); @@ -4257,7 +4249,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * @return {@code true} if starting window is in app's hierarchy. */ boolean hasStartingWindow() { - if (startingDisplayed || mStartingData != null) { + if (mStartingData != null) { return true; } for (int i = mChildren.size() - 1; i >= 0; i--) { @@ -4355,10 +4347,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Transfer the starting window over to the new token. mStartingData = fromActivity.mStartingData; - mSharedStartingData = fromActivity.mSharedStartingData; mStartingSurface = fromActivity.mStartingSurface; - startingDisplayed = fromActivity.startingDisplayed; - fromActivity.startingDisplayed = false; mStartingWindow = tStartingWindow; reportedVisible = fromActivity.reportedVisible; fromActivity.mStartingData = null; @@ -4424,7 +4413,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Moving pending starting from %s to %s", fromActivity, this); mStartingData = fromActivity.mStartingData; - mSharedStartingData = fromActivity.mSharedStartingData; fromActivity.mStartingData = null; fromActivity.startingMoved = true; scheduleAddStartingWindow(); @@ -6534,14 +6522,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Remove starting window directly if is in a pure task. Otherwise if it is associated with // a task (e.g. nested task fragment), then remove only if all visible windows in the task // are drawn. - final Task associatedTask = - mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null; + final Task associatedTask = task.mSharedStartingData != null ? task : null; if (associatedTask == null) { removeStartingWindow(); - } else if (associatedTask.getActivity(r -> r.mVisibleRequested && !r.firstWindowDrawn - // Don't block starting window removal if an Activity can't be a starting window - // target. - && r.mSharedStartingData != null) == null) { + } else if (associatedTask.getActivity( + r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) { // The last drawn activity may not be the one that owns the starting window. final ActivityRecord r = associatedTask.topActivityContainsStartingWindow(); if (r != null) { @@ -6756,7 +6741,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLastTransactionSequence != mWmService.mTransactionSequence) { mLastTransactionSequence = mWmService.mTransactionSequence; mNumDrawnWindows = 0; - startingDisplayed = false; // There is the main base application window, even if it is exiting, wait for it mNumInterestingWindows = findMainWindow(false /* includeStartingApp */) != null ? 1 : 0; @@ -6800,9 +6784,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A isInterestingAndDrawn = true; } } - } else if (w.isDrawn()) { + } else if (mStartingData != null && w.isDrawn()) { // The starting window for this container is drawn. - startingDisplayed = true; + mStartingData.mIsDisplayed = true; } } @@ -7550,7 +7534,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_ANIM, "Animation done in %s" + ": reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b", - this, reportedVisible, okToDisplay(), okToAnimate(), startingDisplayed); + this, reportedVisible, okToDisplay(), okToAnimate(), + isStartingWindowDisplayed()); // clean up thumbnail window if (mThumbnail != null) { @@ -9649,7 +9634,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mStartingWindow != null) { mStartingWindow.writeIdentifierToProto(proto, STARTING_WINDOW); } - proto.write(STARTING_DISPLAYED, startingDisplayed); + proto.write(STARTING_DISPLAYED, isStartingWindowDisplayed()); proto.write(STARTING_MOVED, startingMoved); proto.write(VISIBLE_SET_FROM_TRANSFERRED_STARTING_WINDOW, mVisibleSetFromTransferredStartingWindow); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 027d485d15a0..ed50c1e979ad 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2947,10 +2947,14 @@ class ActivityStarter { } } - // Update the target's launch cookie to those specified in the options if set + // Update the target's launch cookie and pending remote animation to those specified in the + // options if set. if (mStartActivity.mLaunchCookie != null) { intentActivity.mLaunchCookie = mStartActivity.mLaunchCookie; } + if (mStartActivity.mPendingRemoteAnimation != null) { + intentActivity.mPendingRemoteAnimation = mStartActivity.mPendingRemoteAnimation; + } // Need to update mTargetRootTask because if task was moved out of it, the original root // task may be destroyed. diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 9c95e31cc5f5..65fa98f7ab43 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -300,8 +300,8 @@ public class AppTransitionController { layoutRedo = appTransition.goodToGo(transit, topOpeningApp); handleNonAppWindowsInTransition(transit, flags); appTransition.postAnimationCallback(); - appTransition.clear(); } finally { + appTransition.clear(); mService.mSurfaceAnimationRunner.continueStartingAnimations(); } @@ -1229,11 +1229,11 @@ public class AppTransitionController { ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Check opening app=%s: allDrawn=%b startingDisplayed=%b " + "startingMoved=%b isRelaunching()=%b startingWindow=%s", - activity, activity.allDrawn, activity.startingDisplayed, + activity, activity.allDrawn, activity.isStartingWindowDisplayed(), activity.startingMoved, activity.isRelaunching(), activity.mStartingWindow); final boolean allDrawn = activity.allDrawn && !activity.isRelaunching(); - if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) { + if (!allDrawn && !activity.isStartingWindowDisplayed() && !activity.startingMoved) { return false; } if (allDrawn) { diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index fbee343f09b5..300a894d15c9 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -38,6 +38,9 @@ public abstract class StartingData { */ Task mAssociatedTask; + /** Whether the starting window is drawn. */ + boolean mIsDisplayed; + protected StartingData(WindowManagerService service, int typeParams) { mService = service; mTypeParams = typeParams; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index eba49bbc7301..66d7af9b4f6c 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -358,6 +358,13 @@ class Task extends TaskFragment { int mLockTaskUid = -1; // The uid of the application that called startLockTask(). + /** + * If non-null, the starting window should cover the associated task. It is assigned when the + * parent activity of starting window is put in a partial area of the task. This field will be + * cleared when all visible activities in this task are drawn. + */ + StartingData mSharedStartingData; + /** The process that had previously hosted the root activity of this task. * Used to know that we should try harder to keep this process around, in case the * user wants to return to it. */ @@ -3688,6 +3695,9 @@ class Task extends TaskFragment { if (mRootProcess != null) { pw.print(prefix); pw.print("mRootProcess="); pw.println(mRootProcess); } + if (mSharedStartingData != null) { + pw.println(prefix + "mSharedStartingData=" + mSharedStartingData); + } pw.print(prefix); pw.print("taskId=" + mTaskId); pw.println(" rootTaskId=" + getRootTaskId()); pw.print(prefix); pw.println("hasChildPipActivity=" + (mChildPipActivity != null)); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index d17867673474..879c7e2d75dc 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1890,10 +1890,10 @@ class TaskFragment extends WindowContainer<WindowContainer> { RemoteAnimationTarget createRemoteAnimationTarget( RemoteAnimationController.RemoteAnimationRecord record) { final ActivityRecord activity = record.getMode() == RemoteAnimationTarget.MODE_OPENING - // There may be a trampoline activity without window on top of the existing task - // which is moving to front. Exclude the finishing activity so the window of next - // activity can be chosen to create the animation target. - ? getTopNonFinishingActivity() + // There may be a launching (e.g. trampoline or embedded) activity without a window + // on top of the existing task which is moving to front. Exclude finishing activity + // so the window of next activity can be chosen to create the animation target. + ? getActivity(r -> !r.finishing && r.hasChild()) : getTopMostActivity(); return activity != null ? activity.createRemoteAnimationTarget(record) : null; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 744bf0aa42d9..d4c1abfa8d24 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1842,8 +1842,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * @return {@code true} if one or more windows have been displayed, else false. */ boolean hasAppShownWindows() { - return mActivityRecord != null - && (mActivityRecord.firstWindowDrawn || mActivityRecord.startingDisplayed); + return mActivityRecord != null && (mActivityRecord.firstWindowDrawn + || mActivityRecord.isStartingWindowDisplayed()); } @Override diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 6e16b5de1d85..a0ba8fda906f 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -374,13 +374,6 @@ class WindowStateAnimator { } void destroySurfaceLocked(SurfaceControl.Transaction t) { - final ActivityRecord activity = mWin.mActivityRecord; - if (activity != null) { - if (mWin == activity.mStartingWindow) { - activity.startingDisplayed = false; - } - } - if (mSurfaceController == null) { return; } diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java index 7610b7ca5ec3..b33e22fe6d1e 100644 --- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java @@ -164,7 +164,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { } public void testRequestPinAppWidget() { - ComponentName provider = new ComponentName(mTestContext, DummyAppWidget.class); + ComponentName provider = new ComponentName(mTestContext, TestAppWidgetProvider.class); // Set up users. when(mMockShortcutService.requestPinAppWidget(anyString(), any(AppWidgetProviderInfo.class), eq(null), eq(null), anyInt())) @@ -289,6 +289,16 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { assertEquals(4, updates.size()); } + public void testReceiveBroadcastBehavior_enableAndUpdate() { + TestAppWidgetProvider testAppWidgetProvider = new TestAppWidgetProvider(); + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE); + + testAppWidgetProvider.onReceive(mTestContext, intent); + + assertTrue(testAppWidgetProvider.isBehaviorSuccess()); + } + + public void testUpdatesReceived_queueNotEmpty_multipleWidgetIdProvided() { int widgetId = setupHostAndWidget(); int widgetId2 = bindNewWidget(); @@ -385,7 +395,7 @@ public class AppWidgetServiceImplTest extends InstrumentationTestCase { } private int bindNewWidget() { - ComponentName provider = new ComponentName(mTestContext, DummyAppWidget.class); + ComponentName provider = new ComponentName(mTestContext, TestAppWidgetProvider.class); int widgetId = mService.allocateAppWidgetId(mPkgName, HOST_ID); assertTrue(mManager.bindAppWidgetIdIfAllowed(widgetId, provider)); assertEquals(provider, mManager.getAppWidgetInfo(widgetId).provider); diff --git a/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java b/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java new file mode 100644 index 000000000000..6c11a6829c12 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 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.server.appwidget; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; + +/** + * Placeholder widget for testing + */ +public class TestAppWidgetProvider extends AppWidgetProvider { + private boolean mEnabled; + private boolean mUpdated; + + TestAppWidgetProvider() { + super(); + mEnabled = false; + mUpdated = false; + } + + public boolean isBehaviorSuccess() { + return mEnabled && mUpdated; + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, + int[] appWidgetids) { + mUpdated = true; + } + + @Override + public void onEnabled(Context context) { + mEnabled = true; + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 606f4864643c..cd4af0a581f8 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -616,6 +616,20 @@ public class FingerprintAuthenticationClientTest { verify(mCallback).onClientFinished(any(), eq(true)); } + @Test + public void sideFpsPowerPressCancelsIsntantly() throws Exception { + when(mSensorProps.isAnySidefpsType()).thenReturn(true); + + final FingerprintAuthenticationClient client = createClient(1); + client.start(mCallback); + + client.onPowerPressed(); + mLooper.dispatchAll(); + + verify(mCallback, never()).onClientFinished(any(), eq(true)); + verify(mCallback).onClientFinished(any(), eq(false)); + } + private FingerprintAuthenticationClient createClient() throws RemoteException { return createClient(100 /* version */, true /* allowBackgroundAuthentication */); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 462957a88a6c..8a0a4f7c82b7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -2886,6 +2886,7 @@ public class ActivityRecordTests extends WindowTestsBase { fragmentSetup.accept(taskFragment1, new Rect(0, 0, width / 2, height)); task.addChild(taskFragment1, POSITION_TOP); assertEquals(task, activity1.mStartingData.mAssociatedTask); + assertEquals(activity1.mStartingData, task.mSharedStartingData); final TaskFragment taskFragment2 = new TaskFragment( mAtm, null /* fragmentToken */, false /* createdByOrganizer */); @@ -2905,7 +2906,6 @@ public class ActivityRecordTests extends WindowTestsBase { verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl), eq(task.mSurfaceControl)); - assertEquals(activity1.mStartingData, startingWindow.mStartingData); assertEquals(task.mSurfaceControl, startingWindow.getAnimationLeashParent()); assertEquals(taskFragment1.getBounds(), activity1.getBounds()); // The activity was resized by task fragment, but starting window must still cover the task. @@ -2916,6 +2916,7 @@ public class ActivityRecordTests extends WindowTestsBase { activity1.onFirstWindowDrawn(activityWindow); activity2.onFirstWindowDrawn(activityWindow); assertNull(activity1.mStartingWindow); + assertNull(task.mSharedStartingData); } @Test @@ -2991,10 +2992,10 @@ public class ActivityRecordTests extends WindowTestsBase { final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING); final TestWindowState startingWindow = createWindowState(attrs, activity); - activity.startingDisplayed = true; + activity.mStartingData = mock(StartingData.class); activity.addWindow(startingWindow); assertTrue("Starting window should be present", activity.hasStartingWindow()); - activity.startingDisplayed = false; + activity.mStartingData = null; assertTrue("Starting window should be present", activity.hasStartingWindow()); activity.removeChild(startingWindow); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 00be7ed5bc6c..496f6817bb08 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -98,6 +98,7 @@ import android.service.voice.IVoiceInteractionSession; import android.util.Pair; import android.util.Size; import android.view.Gravity; +import android.view.RemoteAnimationAdapter; import android.window.TaskFragmentOrganizerToken; import androidx.test.filters.SmallTest; @@ -1315,6 +1316,32 @@ public class ActivityStarterTests extends WindowTestsBase { } @Test + public void testRemoteAnimation_appliesToExistingTask() { + final ActivityStarter starter = prepareStarter(0, false); + + // Put an activity on default display as the top focused activity. + ActivityRecord r = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Intent intent = new Intent(); + intent.setComponent(ActivityBuilder.getDefaultComponent()); + starter.setReason("testRemoteAnimation_newTask") + .setIntent(intent) + .execute(); + + assertNull(mRootWindowContainer.topRunningActivity().mPendingRemoteAnimation); + + // Relaunch the activity with remote animation indicated in options. + final RemoteAnimationAdapter adaptor = mock(RemoteAnimationAdapter.class); + final ActivityOptions options = ActivityOptions.makeRemoteAnimation(adaptor); + starter.setReason("testRemoteAnimation_existingTask") + .setIntent(intent) + .setActivityOptions(options.toBundle()) + .execute(); + + // Verify the remote animation is updated. + assertEquals(adaptor, mRootWindowContainer.topRunningActivity().mPendingRemoteAnimation); + } + + @Test public void testStartLaunchIntoPipActivity() { final ActivityStarter starter = prepareStarter(0, false); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java index 513791d2b8a5..0332c4b4597b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java @@ -1300,6 +1300,8 @@ public class AppTransitionControllerTest extends WindowTestsBase { activity.allDrawn = true; // Skip manipulate the SurfaceControl. doNothing().when(activity).setDropInputMode(anyInt()); + // Assume the activity contains a window. + doReturn(true).when(activity).hasChild(); // Make sure activity can create remote animation target. doReturn(mock(RemoteAnimationTarget.class)).when(activity).createRemoteAnimationTarget( any()); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java index f61effa284ef..d55e53cca8a6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java @@ -321,7 +321,6 @@ public class AppTransitionTests extends WindowTestsBase { final ActivityRecord activity2 = createActivityRecord(dc2); activity1.allDrawn = true; - activity1.startingDisplayed = true; activity1.startingMoved = true; // Simulate activity resume / finish flows to prepare app transition & set visibility, diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 352d8d115b6a..bc5c9ec7a29c 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -172,11 +172,11 @@ public class VoiceInteractionManagerService extends SystemService { mAmInternal.setVoiceInteractionManagerProvider( new ActivityManagerInternal.VoiceInteractionManagerProvider() { @Override - public void notifyActivityEventChanged() { + public void notifyActivityDestroyed(IBinder activityToken) { if (DEBUG) { - Slog.d(TAG, "call notifyActivityEventChanged"); + Slog.d(TAG, "notifyActivityDestroyed activityToken=" + activityToken); } - mServiceStub.notifyActivityEventChanged(); + mServiceStub.notifyActivityDestroyed(activityToken); } }); } @@ -447,11 +447,12 @@ public class VoiceInteractionManagerService extends SystemService { return mImpl.supportsLocalVoiceInteraction(); } - void notifyActivityEventChanged() { + void notifyActivityDestroyed(@NonNull IBinder activityToken) { synchronized (this) { - if (mImpl == null) return; + if (mImpl == null || activityToken == null) return; - Binder.withCleanCallingIdentity(() -> mImpl.notifyActivityEventChangedLocked()); + Binder.withCleanCallingIdentity( + () -> mImpl.notifyActivityDestroyedLocked(activityToken)); } } @@ -1223,6 +1224,16 @@ public class VoiceInteractionManagerService extends SystemService { } } + @Override + public void notifyActivityEventChanged(@NonNull IBinder activityToken, int type) { + synchronized (this) { + if (mImpl == null || activityToken == null) { + return; + } + Binder.withCleanCallingIdentity( + () -> mImpl.notifyActivityEventChangedLocked(activityToken, type)); + } + } //----------------- Hotword Detection/Validation APIs --------------------------------// @Override diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index b9793cafb6fe..fabab2524e83 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -520,9 +520,23 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne mActiveSession.stopListeningVisibleActivityChangedLocked(); } - public void notifyActivityEventChangedLocked() { + public void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) { if (DEBUG) { - Slog.d(TAG, "notifyActivityEventChangedLocked"); + Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken); + } + if (mActiveSession == null || !mActiveSession.mShown) { + if (DEBUG) { + Slog.d(TAG, "notifyActivityDestroyedLocked not allowed on no session or" + + " hidden session"); + } + return; + } + mActiveSession.notifyActivityDestroyedLocked(activityToken); + } + + public void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) { + if (DEBUG) { + Slog.d(TAG, "notifyActivityEventChangedLocked type=" + type); } if (mActiveSession == null || !mActiveSession.mShown) { if (DEBUG) { @@ -531,7 +545,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne } return; } - mActiveSession.notifyActivityEventChangedLocked(); + mActiveSession.notifyActivityEventChangedLocked(activityToken, type); } public void updateStateLocked( diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java index ae9be8c09e60..b24337fd54c9 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java @@ -29,6 +29,7 @@ import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_DATA; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_TASK_ID; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AppOpsManager; @@ -59,6 +60,7 @@ import android.service.voice.IVoiceInteractionSessionService; import android.service.voice.VisibleActivityInfo; import android.service.voice.VoiceInteractionService; import android.service.voice.VoiceInteractionSession; +import android.util.ArrayMap; import android.util.Slog; import android.view.IWindowManager; @@ -128,7 +130,11 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, private boolean mListeningVisibleActivity; private final ScheduledExecutorService mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); - private final List<VisibleActivityInfo> mVisibleActivityInfos = new ArrayList<>(); + // Records the visible activity information the system has already called onVisible, without + // confirming the result of callback. When activity visible state is changed, we use this to + // determine to call onVisible or onInvisible to assistant application. + private final ArrayMap<IBinder, VisibleActivityInfo> mVisibleActivityInfoForToken = + new ArrayMap<>(); private final PowerManagerInternal mPowerManagerInternal; private final LowPowerStandbyControllerInternal mLowPowerStandbyControllerInternal; private final Runnable mRemoveFromLowPowerStandbyAllowlistRunnable = @@ -530,7 +536,7 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, public void cancelLocked(boolean finishTask) { mListeningVisibleActivity = false; - mVisibleActivityInfos.clear(); + mVisibleActivityInfoForToken.clear(); hideLocked(); mCanceled = true; if (mBound) { @@ -608,17 +614,24 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, if (DEBUG) { Slog.d(TAG, "startListeningVisibleActivityChangedLocked"); } + + if (!mShown || mCanceled || mSession == null) { + return; + } + mListeningVisibleActivity = true; - mVisibleActivityInfos.clear(); + mVisibleActivityInfoForToken.clear(); - mScheduledExecutorService.execute(() -> { - if (DEBUG) { - Slog.d(TAG, "call handleVisibleActivitiesLocked from enable listening"); - } - synchronized (mLock) { - handleVisibleActivitiesLocked(); - } - }); + // It should only need to report which activities are visible + final ArrayMap<IBinder, VisibleActivityInfo> newVisibleActivityInfos = + getTopVisibleActivityInfosLocked(); + + if (newVisibleActivityInfos == null || newVisibleActivityInfos.isEmpty()) { + return; + } + notifyVisibleActivitiesChangedLocked(newVisibleActivityInfos, + VisibleActivityInfo.TYPE_ACTIVITY_ADDED); + mVisibleActivityInfoForToken.putAll(newVisibleActivityInfos); } void stopListeningVisibleActivityChangedLocked() { @@ -626,12 +639,13 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, Slog.d(TAG, "stopListeningVisibleActivityChangedLocked"); } mListeningVisibleActivity = false; - mVisibleActivityInfos.clear(); + mVisibleActivityInfoForToken.clear(); } - void notifyActivityEventChangedLocked() { + void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) { if (DEBUG) { - Slog.d(TAG, "notifyActivityEventChangedLocked"); + Slog.d(TAG, "notifyActivityEventChangedLocked activityToken=" + activityToken + + ", type=" + type); } if (!mListeningVisibleActivity) { if (DEBUG) { @@ -640,99 +654,139 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, return; } mScheduledExecutorService.execute(() -> { - if (DEBUG) { - Slog.d(TAG, "call handleVisibleActivitiesLocked from activity event"); - } synchronized (mLock) { - handleVisibleActivitiesLocked(); + handleVisibleActivitiesLocked(activityToken, type); } }); } - private List<VisibleActivityInfo> getVisibleActivityInfosLocked() { + private ArrayMap<IBinder, VisibleActivityInfo> getTopVisibleActivityInfosLocked() { if (DEBUG) { - Slog.d(TAG, "getVisibleActivityInfosLocked"); + Slog.d(TAG, "getTopVisibleActivityInfosLocked"); } List<ActivityAssistInfo> allVisibleActivities = LocalServices.getService(ActivityTaskManagerInternal.class) .getTopVisibleActivities(); if (DEBUG) { - Slog.d(TAG, - "getVisibleActivityInfosLocked: allVisibleActivities=" + allVisibleActivities); + Slog.d(TAG, "getTopVisibleActivityInfosLocked: allVisibleActivities=" + + allVisibleActivities); } - if (allVisibleActivities == null || allVisibleActivities.isEmpty()) { + if (allVisibleActivities.isEmpty()) { Slog.w(TAG, "no visible activity"); return null; } final int count = allVisibleActivities.size(); - final List<VisibleActivityInfo> visibleActivityInfos = new ArrayList<>(count); + final ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfoArrayMap = + new ArrayMap<>(count); for (int i = 0; i < count; i++) { ActivityAssistInfo info = allVisibleActivities.get(i); if (DEBUG) { - Slog.d(TAG, " : activityToken=" + info.getActivityToken() + Slog.d(TAG, "ActivityAssistInfo : activityToken=" + info.getActivityToken() + ", assistToken=" + info.getAssistToken() + ", taskId=" + info.getTaskId()); } - visibleActivityInfos.add( + visibleActivityInfoArrayMap.put(info.getActivityToken(), new VisibleActivityInfo(info.getTaskId(), info.getAssistToken())); } - return visibleActivityInfos; + return visibleActivityInfoArrayMap; } - private void handleVisibleActivitiesLocked() { + // TODO(b/242359988): Split this method up + private void handleVisibleActivitiesLocked(@NonNull IBinder activityToken, int type) { if (DEBUG) { - Slog.d(TAG, "handleVisibleActivitiesLocked"); + Slog.d(TAG, "handleVisibleActivitiesLocked activityToken=" + activityToken + + ", type=" + type); } - if (mSession == null) { + + if (!mListeningVisibleActivity) { + if (DEBUG) { + Slog.d(TAG, "not enable listening visible activity"); + } return; } - if (!mShown || !mListeningVisibleActivity || mCanceled) { + if (!mShown || mCanceled || mSession == null) { return; } - final List<VisibleActivityInfo> newVisibleActivityInfos = getVisibleActivityInfosLocked(); - if (newVisibleActivityInfos == null || newVisibleActivityInfos.isEmpty()) { - notifyVisibleActivitiesChangedLocked(mVisibleActivityInfos, - VisibleActivityInfo.TYPE_ACTIVITY_REMOVED); - mVisibleActivityInfos.clear(); - return; - } - if (mVisibleActivityInfos.isEmpty()) { - notifyVisibleActivitiesChangedLocked(newVisibleActivityInfos, - VisibleActivityInfo.TYPE_ACTIVITY_ADDED); - mVisibleActivityInfos.addAll(newVisibleActivityInfos); + // We use this local variable to determine to call onVisible or onInvisible. + boolean notifyOnVisible = false; + VisibleActivityInfo notifyVisibleActivityInfo = null; + + if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_START + || type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_RESUME) { + // It seems that the onStart is unnecessary. But if we have it, the assistant + // application can request the directActions early. Even if we have the onStart, + // we still need the onResume because it is possible that the activity goes to + // onResume from onPause with invisible before the activity goes to onStop from + // onPause. + + // Check if we have reported this activity as visible. If we have reported it as + // visible, do nothing. + if (mVisibleActivityInfoForToken.containsKey(activityToken)) { + return; + } + + // Before reporting this activity as visible, we need to make sure the activity + // is really visible. + notifyVisibleActivityInfo = getVisibleActivityInfoFromTopVisibleActivity( + activityToken); + if (notifyVisibleActivityInfo == null) { + return; + } + notifyOnVisible = true; + } else if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE) { + // For the onPause stage, the Activity is not necessarily invisible now, so we need + // to check its state. + // Note: After syncing with Activity owner, before the onPause is called, the + // visibility state has been updated. + notifyVisibleActivityInfo = getVisibleActivityInfoFromTopVisibleActivity( + activityToken); + if (notifyVisibleActivityInfo != null) { + return; + } + + // Also make sure we previously reported this Activity as visible. + notifyVisibleActivityInfo = mVisibleActivityInfoForToken.get(activityToken); + if (notifyVisibleActivityInfo == null) { + return; + } + } else if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP) { + // For the onStop stage, the activity is in invisible state. We only need to consider if + // we have reported this activity as visible. If we have reported it as visible, we + // need to report it as invisible. + // Why we still need onStop? Because it is possible that the activity is in a visible + // state during onPause stage, when the activity enters onStop from onPause, we may + // need to notify onInvisible. + // Note: After syncing with Activity owner, before the onStop is called, the + // visibility state has been updated. + notifyVisibleActivityInfo = mVisibleActivityInfoForToken.get(activityToken); + if (notifyVisibleActivityInfo == null) { + return; + } + } else { + Slog.w(TAG, "notifyActivityEventChangedLocked unexpected type=" + type); return; } - final List<VisibleActivityInfo> addedActivities = new ArrayList<>(); - final List<VisibleActivityInfo> removedActivities = new ArrayList<>(); - - removedActivities.addAll(mVisibleActivityInfos); - for (int i = 0; i < newVisibleActivityInfos.size(); i++) { - final VisibleActivityInfo candidateVisibleActivityInfo = newVisibleActivityInfos.get(i); - if (!removedActivities.isEmpty() && removedActivities.contains( - candidateVisibleActivityInfo)) { - removedActivities.remove(candidateVisibleActivityInfo); - } else { - addedActivities.add(candidateVisibleActivityInfo); + try { + mSession.notifyVisibleActivityInfoChanged(notifyVisibleActivityInfo, + notifyOnVisible ? VisibleActivityInfo.TYPE_ACTIVITY_ADDED + : VisibleActivityInfo.TYPE_ACTIVITY_REMOVED); + } catch (RemoteException e) { + if (DEBUG) { + Slog.w(TAG, "handleVisibleActivitiesLocked RemoteException : " + e); } } - if (!addedActivities.isEmpty()) { - notifyVisibleActivitiesChangedLocked(addedActivities, - VisibleActivityInfo.TYPE_ACTIVITY_ADDED); - } - if (!removedActivities.isEmpty()) { - notifyVisibleActivitiesChangedLocked(removedActivities, - VisibleActivityInfo.TYPE_ACTIVITY_REMOVED); + if (notifyOnVisible) { + mVisibleActivityInfoForToken.put(activityToken, notifyVisibleActivityInfo); + } else { + mVisibleActivityInfoForToken.remove(activityToken); } - - mVisibleActivityInfos.clear(); - mVisibleActivityInfos.addAll(newVisibleActivityInfos); } private void notifyVisibleActivitiesChangedLocked( - List<VisibleActivityInfo> visibleActivityInfos, int type) { + ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfos, int type) { if (visibleActivityInfos == null || visibleActivityInfos.isEmpty()) { return; } @@ -741,7 +795,7 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, } try { for (int i = 0; i < visibleActivityInfos.size(); i++) { - mSession.notifyVisibleActivityInfoChanged(visibleActivityInfos.get(i), type); + mSession.notifyVisibleActivityInfoChanged(visibleActivityInfos.valueAt(i), type); } } catch (RemoteException e) { if (DEBUG) { @@ -754,6 +808,51 @@ final class VoiceInteractionSessionConnection implements ServiceConnection, } } + private VisibleActivityInfo getVisibleActivityInfoFromTopVisibleActivity( + @NonNull IBinder activityToken) { + final ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfos = + getTopVisibleActivityInfosLocked(); + if (visibleActivityInfos == null) { + return null; + } + return visibleActivityInfos.get(activityToken); + } + + void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) { + if (DEBUG) { + Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken); + } + if (!mListeningVisibleActivity) { + if (DEBUG) { + Slog.d(TAG, "not enable listening visible activity"); + } + return; + } + mScheduledExecutorService.execute(() -> { + synchronized (mLock) { + if (!mListeningVisibleActivity) { + return; + } + if (!mShown || mCanceled || mSession == null) { + return; + } + + VisibleActivityInfo visibleActivityInfo = mVisibleActivityInfoForToken.remove( + activityToken); + if (visibleActivityInfo != null) { + try { + mSession.notifyVisibleActivityInfoChanged(visibleActivityInfo, + VisibleActivityInfo.TYPE_ACTIVITY_REMOVED); + } catch (RemoteException e) { + if (DEBUG) { + Slog.w(TAG, "notifyVisibleActivityInfoChanged RemoteException : " + e); + } + } + } + } + }); + } + private void removeFromLowPowerStandbyAllowlist() { synchronized (mLock) { if (mLowPowerStandbyAllowlisted) { |