diff options
288 files changed, 7637 insertions, 2110 deletions
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java index 2d2cf1c80e1e..b04d08f6795f 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/InTaskTransitionTest.java @@ -34,11 +34,20 @@ import android.view.WindowManagerGlobal; import org.junit.Rule; import org.junit.Test; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + /** Measure the performance of warm launch activity in the same task. */ public class InTaskTransitionTest extends WindowManagerPerfTestBase implements RemoteCallback.OnResultListener { private static final long TIMEOUT_MS = 5000; + private static final String LOG_SEPARATOR = "LOG_SEPARATOR"; @Rule public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter(); @@ -62,6 +71,7 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState(); long measuredTimeNs = 0; + long firstStartTime = 0; boolean readerStarted = false; while (state.keepRunning(measuredTimeNs)) { @@ -70,6 +80,10 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase readerStarted = true; } final long startTime = SystemClock.elapsedRealtimeNanos(); + if (readerStarted && firstStartTime == 0) { + firstStartTime = startTime; + executeShellCommand("log -t " + LOG_SEPARATOR + " " + firstStartTime); + } activity.startActivity(next); synchronized (mMetricsReader) { try { @@ -89,6 +103,7 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase state.addExtraResult("windowsDrawnDelayMs", metrics.mWindowsDrawnDelayMs); } } + addExtraTransitionInfo(firstStartTime, state); } @Override @@ -99,6 +114,46 @@ public class InTaskTransitionTest extends WindowManagerPerfTestBase } } + private void addExtraTransitionInfo(long startTime, ManualBenchmarkState state) { + final ProcessBuilder pb = new ProcessBuilder("sh"); + final String startLine = String.valueOf(startTime); + final String commitTimeStr = " commit="; + boolean foundStartLine = false; + try { + final Process process = pb.start(); + final InputStream in = process.getInputStream(); + final PrintWriter out = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(process.getOutputStream())), true /* autoFlush */); + out.println("logcat -v brief -d *:S WindowManager:V " + LOG_SEPARATOR + ":I" + + " | grep -e 'Finish Transition' -e " + LOG_SEPARATOR); + out.println("exit"); + + String line; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + while ((line = reader.readLine()) != null) { + if (!foundStartLine) { + if (line.contains(startLine)) { + foundStartLine = true; + } + continue; + } + final int strPos = line.indexOf(commitTimeStr); + if (strPos < 0) { + continue; + } + final int endPos = line.indexOf("ms", strPos); + if (endPos > strPos) { + final int commitDelayMs = Math.round(Float.parseFloat( + line.substring(strPos + commitTimeStr.length(), endPos))); + state.addExtraResult("commitDelayMs", commitDelayMs); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + /** The test activity runs on a different process to trigger metrics logs. */ public static class TestActivity extends Activity implements Runnable { static final String CALLBACK = "callback"; diff --git a/core/api/current.txt b/core/api/current.txt index c4109392d6bd..6707c15de682 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8905,7 +8905,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); + method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<android.app.appfunctions.ExecuteAppFunctionResponse,android.app.appfunctions.AppFunctionException>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0e833bdba1e1..93f311969c1e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -581,7 +581,7 @@ package android.accessibilityservice { package android.accounts { public class AccountManager { - method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>, @Nullable android.os.Handler); + method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.os.Handler, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.accounts.AccountManagerFuture<android.os.Bundle> finishSessionAsUser(android.os.Bundle, android.app.Activity, android.os.UserHandle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler); } @@ -18570,13 +18570,13 @@ package android.telephony.satellite { @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons(); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAccessStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a988acf1f4a9..a352d9d2ea06 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3415,6 +3415,10 @@ package android.telecom { method public void onBindClient(@Nullable android.content.Intent); } + public class TelecomManager { + method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle); + } + } package android.telephony { diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 72450999993d..ddc1ae29f6df 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -30,7 +30,6 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.Size; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; @@ -2019,23 +2018,22 @@ public class AccountManager { * @param account the account to copy * @param fromUser the user to copy the account from * @param toUser the target user - * @param callback Callback to invoke when the request completes, - * null for no callback * @param handler {@link Handler} identifying the callback thread, * null for the main thread + * @param callback Callback to invoke when the request completes, + * null for no callback * @return An {@link AccountManagerFuture} which resolves to a Boolean indicated whether it * succeeded. * @hide */ - @SuppressLint("SamShouldBeLast") @NonNull @SystemApi @RequiresPermission(anyOf = {COPY_ACCOUNTS, INTERACT_ACROSS_USERS_FULL}) @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) public AccountManagerFuture<Boolean> copyAccountToUser( @NonNull final Account account, @NonNull final UserHandle fromUser, - @NonNull final UserHandle toUser, @Nullable AccountManagerCallback<Boolean> callback, - @Nullable Handler handler) { + @NonNull final UserHandle toUser, @Nullable Handler handler, + @Nullable AccountManagerCallback<Boolean> callback) { if (account == null) throw new IllegalArgumentException("account is null"); if (toUser == null || fromUser == null) { throw new IllegalArgumentException("fromUser and toUser cannot be null"); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index b198811416cd..4782205e3b21 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1027,9 +1027,6 @@ public class Activity extends ContextThemeWrapper /** The autofill client controller. Always access via {@link #getAutofillClientController()}. */ private AutofillClientController mAutofillClientController; - /** @hide */ - boolean mEnterAnimationComplete; - private boolean mIsInMultiWindowMode; /** @hide */ boolean mIsInPictureInPictureMode; @@ -2898,7 +2895,6 @@ public class Activity extends ContextThemeWrapper mCalled = true; getAutofillClientController().onActivityStopped(mIntent, mChangingConfigurations); - mEnterAnimationComplete = false; notifyVoiceInteractionManagerServiceActivityEvent( VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP); @@ -8594,8 +8590,6 @@ public class Activity extends ContextThemeWrapper * @hide */ public void dispatchEnterAnimationComplete() { - mEnterAnimationComplete = true; - mInstrumentation.onEnterAnimationComplete(); onEnterAnimationComplete(); if (getWindow() != null && getWindow().getDecorView() != null) { View decorView = getWindow().getDecorView(); diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 7eacaac29d4b..b611acf79bc3 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -59,7 +59,6 @@ import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.SurfaceControl; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManagerGlobal; @@ -137,7 +136,6 @@ public class Instrumentation { private PerformanceCollector mPerformanceCollector; private Bundle mPerfMetrics = new Bundle(); private UiAutomation mUiAutomation; - private final Object mAnimationCompleteLock = new Object(); @RavenwoodKeep public Instrumentation() { @@ -455,31 +453,6 @@ public class Instrumentation { idler.waitForIdle(); } - private void waitForEnterAnimationComplete(Activity activity) { - synchronized (mAnimationCompleteLock) { - long timeout = 5000; - try { - // We need to check that this specified Activity completed the animation, not just - // any Activity. If it was another Activity, then decrease the timeout by how long - // it's already waited and wait for the thread to wakeup again. - while (timeout > 0 && !activity.mEnterAnimationComplete) { - long startTime = System.currentTimeMillis(); - mAnimationCompleteLock.wait(timeout); - long totalTime = System.currentTimeMillis() - startTime; - timeout -= totalTime; - } - } catch (InterruptedException e) { - } - } - } - - /** @hide */ - public void onEnterAnimationComplete() { - synchronized (mAnimationCompleteLock) { - mAnimationCompleteLock.notifyAll(); - } - } - /** * Execute a call on the application's main thread, blocking until it is * complete. Useful for doing things that are not thread-safe, such as @@ -640,13 +613,14 @@ public class Instrumentation { activity = aw.activity; } - // Do not call this method within mSync, lest it could block the main thread. - waitForEnterAnimationComplete(activity); - - // Apply an empty transaction to ensure SF has a chance to update before - // the Activity is ready (b/138263890). - try (SurfaceControl.Transaction t = new SurfaceControl.Transaction()) { - t.apply(true); + // Typically, callers expect that the launched activity can receive input events after this + // method returns, so wait until a stable state, i.e. animation is finished and input info + // is updated. + try { + WindowManagerGlobal.getWindowManagerService() + .syncInputTransactions(true /* waitForAnimations */); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } return activity; } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 614e2aaf42e8..5176aee9051f 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5856,7 +5856,9 @@ public class Notification implements Parcelable return null; } final int size = mContext.getResources().getDimensionPixelSize( - R.dimen.notification_badge_size); + Flags.notificationsRedesignTemplates() + ? R.dimen.notification_2025_badge_size + : R.dimen.notification_badge_size); Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); badge.setBounds(0, 0, size, size); diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 08bd854525ec..aede8aa70ede 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,6 +17,7 @@ package android.app; import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.notificationClassification; @@ -50,6 +51,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -71,6 +73,8 @@ import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.InstantSource; @@ -1202,12 +1206,20 @@ public class NotificationManager { * package (see {@link Context#createPackageContext(String, int)}).</p> */ public NotificationChannel getNotificationChannel(String channelId) { - INotificationManager service = service(); - try { - return service.getNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getChannelFromList(channelId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1222,13 +1234,21 @@ public class NotificationManager { */ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId, @NonNull String conversationId) { - INotificationManager service = service(); - try { - return service.getConversationNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId, true, - conversationId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getConversationChannelFromList(channelId, conversationId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getConversationNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId, true, + conversationId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1241,15 +1261,62 @@ public class NotificationManager { * {@link Context#createPackageContext(String, int)}).</p> */ public List<NotificationChannel> getNotificationChannels() { - INotificationManager service = service(); - try { - return service.getNotificationChannels(mContext.getOpPackageName(), - mContext.getPackageName(), mContext.getUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId())); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannels(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId()).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } + // channel list assumed to be associated with the appropriate package & user id already. + private static NotificationChannel getChannelFromList(String channelId, + List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + for (NotificationChannel channel : channels) { + if (channelId.equals(channel.getId())) { + return channel; + } + } + return null; + } + + private static NotificationChannel getConversationChannelFromList(String channelId, + String conversationId, List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + if (conversationId == null) { + return getChannelFromList(channelId, channels); + } + NotificationChannel parent = null; + for (NotificationChannel channel : channels) { + if (conversationId.equals(channel.getConversationId()) + && channelId.equals(channel.getParentChannelId())) { + return channel; + } else if (channelId.equals(channel.getId())) { + parent = channel; + } + } + return parent; + } + /** * Deletes the given notification channel. * @@ -1328,6 +1395,71 @@ public class NotificationManager { } } + private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel"; + private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels"; + private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10; + + private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() { + @Override + public List<NotificationChannel> apply(NotificationChannelQuery query) { + INotificationManager service = service(); + try { + return service.getNotificationChannels(query.callingPkg, + query.targetPkg, query.userId).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) { + // Other locations should also not be querying the cache in the first place if + // the flag is not enabled, but this is an extra precaution. + if (!Flags.nmBinderPerfCacheChannels()) { + Log.wtf(TAG, + "shouldBypassCache called when nm_binder_perf_cache_channels off"); + return true; + } + return false; + } + }; + + private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListCache = + new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME, + mNotificationChannelListQueryHandler); + + private record NotificationChannelQuery( + String callingPkg, + String targetPkg, + int userId) {} + + /** + * @hide + */ + public static void invalidateNotificationChannelCache() { + if (Flags.nmBinderPerfCacheChannels()) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API); + } else { + // if we are here, we have failed to flag something + Log.wtf(TAG, "invalidateNotificationChannelCache called without flag"); + } + } + + /** + * For testing only: running tests with a cache requires marking the cache's property for + * testing, as test APIs otherwise cannot invalidate the cache. This must be called after + * calling PropertyInvalidatedCache.setTestMode(true). + * @hide + */ + @VisibleForTesting + public void setChannelCacheToTestMode() { + mNotificationChannelListCache.testPropertyName(); + } + /** * @hide */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a2fddb045179..c50452157d74 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4354,20 +4354,24 @@ public class DevicePolicyManager { } /** - * Indicates that app functions are not controlled by policy. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by + * policy. * * <p>If no admin set this policy, it means appfunctions are enabled. */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0; - /** Indicates that app functions are controlled and disabled by a policy. */ + /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by policy, i.e. no apps in the current user are allowed to expose app functions. + */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_DISABLED = 1; /** - * Indicates that app functions are controlled and disabled by a policy for cross profile - * interactions only. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps + * in the current user can only be invoked within the same user. * * <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross * profile interactions (even if the caller has permissions required to interact across users). @@ -4388,7 +4392,9 @@ public class DevicePolicyManager { public @interface AppFunctionsPolicy {} /** - * Sets the app functions policy which controls app functions operations on the device. + * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app + * functions operations on the device. An app function is a piece of functionality that apps + * expose to the system for cross-app orchestration. * * <p>This function can only be called by a device owner, a profile owner or holders of the * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}. @@ -4414,7 +4420,7 @@ public class DevicePolicyManager { } /** - * Returns the current app functions policy. + * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy. * * <p>The returned policy will be the current resolved policy rather than the policy set by the * calling admin. diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index d86f1d841d33..8e48b4e56570 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -28,6 +28,7 @@ import android.annotation.SdkConstant; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -78,10 +79,10 @@ public abstract class AppFunctionService extends Service { void perform( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull CancellationSignal cancellationSignal, @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); } /** @hide */ @@ -93,6 +94,7 @@ public abstract class AppFunctionService extends Service { public void executeAppFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull ICancellationCallback cancellationCallback, @NonNull IExecuteAppFunctionCallback callback) { if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE) @@ -105,6 +107,7 @@ public abstract class AppFunctionService extends Service { onExecuteFunction.perform( request, callingPackage, + callingPackageSigningInfo, buildCancellationSignal(cancellationCallback), new OutcomeReceiver<>() { @Override @@ -154,15 +157,17 @@ public abstract class AppFunctionService extends Service { /** * Called by the system to execute a specific app function. * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. + * <p>This method is the entry point for handling all app function requests in an app. When the + * system needs your AppFunctionService to perform a function, it will invoke this method. * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * <p>Each function you've registered is identified by a unique identifier. This identifier + * doesn't need to be globally unique, but it must be unique within your app. For example, a + * function to order food could be identified as "orderFood". In most cases, this identifier is + * automatically generated by the AppFunctions SDK. + * + * <p>You can determine which function to execute by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the + * incoming request to the appropriate logic for handling the specific function. * * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker * thread and dispatch the result with the given callback. You should always report back the @@ -173,6 +178,8 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. + * @param callingPackageSigningInfo The signing information of the app that is requesting the + * execution. * @param cancellationSignal A signal to cancel the execution. * @param callback A callback to report back the result or error. */ @@ -180,10 +187,9 @@ public abstract class AppFunctionService extends Service { public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, + @NonNull SigningInfo callingPackageSigningInfo, @NonNull CancellationSignal cancellationSignal, - @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); /** * Returns result codes from throwable. diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl index bf935d2a102b..78bcb7f66eb1 100644 --- a/core/java/android/app/appfunctions/IAppFunctionService.aidl +++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl @@ -35,12 +35,15 @@ oneway interface IAppFunctionService { * * @param request the function execution request. * @param callingPackage The package name of the app that is requesting the execution. + * @param callingPackageSigningInfo The signing information of the app that is requesting the + * execution. * @param cancellationCallback a callback to send back the cancellation transport. * @param callback a callback to report back the result. */ void executeAppFunction( in ExecuteAppFunctionRequest request, in String callingPackage, + in android.content.pm.SigningInfo callingPackageSigningInfo, in ICancellationCallback cancellationCallback, in IExecuteAppFunctionCallback callback ); diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java index 4dee15988795..929f66079a3d 100644 --- a/core/java/android/app/time/TimeZoneCapabilities.java +++ b/core/java/android/app/time/TimeZoneCapabilities.java @@ -55,7 +55,8 @@ public final class TimeZoneCapabilities implements Parcelable { * The user the capabilities are for. This is used for object equality and debugging but there * is no accessor. */ - @NonNull private final UserHandle mUserHandle; + @NonNull + private final UserHandle mUserHandle; private final @CapabilityState int mConfigureAutoDetectionEnabledCapability; /** @@ -69,6 +70,7 @@ public final class TimeZoneCapabilities implements Parcelable { private final @CapabilityState int mConfigureGeoDetectionEnabledCapability; private final @CapabilityState int mSetManualTimeZoneCapability; + private final @CapabilityState int mConfigureNotificationsEnabledCapability; private TimeZoneCapabilities(@NonNull Builder builder) { this.mUserHandle = Objects.requireNonNull(builder.mUserHandle); @@ -78,6 +80,8 @@ public final class TimeZoneCapabilities implements Parcelable { this.mConfigureGeoDetectionEnabledCapability = builder.mConfigureGeoDetectionEnabledCapability; this.mSetManualTimeZoneCapability = builder.mSetManualTimeZoneCapability; + this.mConfigureNotificationsEnabledCapability = + builder.mConfigureNotificationsEnabledCapability; } @NonNull @@ -88,6 +92,7 @@ public final class TimeZoneCapabilities implements Parcelable { .setUseLocationEnabled(in.readBoolean()) .setConfigureGeoDetectionEnabledCapability(in.readInt()) .setSetManualTimeZoneCapability(in.readInt()) + .setConfigureNotificationsEnabledCapability(in.readInt()) .build(); } @@ -98,6 +103,7 @@ public final class TimeZoneCapabilities implements Parcelable { dest.writeBoolean(mUseLocationEnabled); dest.writeInt(mConfigureGeoDetectionEnabledCapability); dest.writeInt(mSetManualTimeZoneCapability); + dest.writeInt(mConfigureNotificationsEnabledCapability); } /** @@ -117,8 +123,8 @@ public final class TimeZoneCapabilities implements Parcelable { * * Not part of the SDK API because it is intended for use by SettingsUI, which can display * text about needing it to be on for location-based time zone detection. - * @hide * + * @hide */ public boolean isUseLocationEnabled() { return mUseLocationEnabled; @@ -148,6 +154,18 @@ public final class TimeZoneCapabilities implements Parcelable { } /** + * Returns the capability state associated with the user's ability to modify the time zone + * notification setting. The setting can be updated via {@link + * TimeManager#updateTimeZoneConfiguration(TimeZoneConfiguration)}. + * + * @hide + */ + @CapabilityState + public int getConfigureNotificationsEnabledCapability() { + return mConfigureNotificationsEnabledCapability; + } + + /** * Tries to create a new {@link TimeZoneConfiguration} from the {@code config} and the set of * {@code requestedChanges}, if {@code this} capabilities allow. The new configuration is * returned. If the capabilities do not permit one or more of the requested changes then {@code @@ -174,6 +192,12 @@ public final class TimeZoneCapabilities implements Parcelable { newConfigBuilder.setGeoDetectionEnabled(requestedChanges.isGeoDetectionEnabled()); } + if (requestedChanges.hasIsNotificationsEnabled()) { + if (this.getConfigureNotificationsEnabledCapability() < CAPABILITY_NOT_APPLICABLE) { + return null; + } + newConfigBuilder.setNotificationsEnabled(requestedChanges.areNotificationsEnabled()); + } return newConfigBuilder.build(); } @@ -197,13 +221,16 @@ public final class TimeZoneCapabilities implements Parcelable { && mUseLocationEnabled == that.mUseLocationEnabled && mConfigureGeoDetectionEnabledCapability == that.mConfigureGeoDetectionEnabledCapability - && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability; + && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability + && mConfigureNotificationsEnabledCapability + == that.mConfigureNotificationsEnabledCapability; } @Override public int hashCode() { return Objects.hash(mUserHandle, mConfigureAutoDetectionEnabledCapability, - mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability); + mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability, + mConfigureNotificationsEnabledCapability); } @Override @@ -216,6 +243,8 @@ public final class TimeZoneCapabilities implements Parcelable { + ", mConfigureGeoDetectionEnabledCapability=" + mConfigureGeoDetectionEnabledCapability + ", mSetManualTimeZoneCapability=" + mSetManualTimeZoneCapability + + ", mConfigureNotificationsEnabledCapability=" + + mConfigureNotificationsEnabledCapability + '}'; } @@ -226,11 +255,13 @@ public final class TimeZoneCapabilities implements Parcelable { */ public static class Builder { - @NonNull private UserHandle mUserHandle; + @NonNull + private UserHandle mUserHandle; private @CapabilityState int mConfigureAutoDetectionEnabledCapability; private Boolean mUseLocationEnabled; private @CapabilityState int mConfigureGeoDetectionEnabledCapability; private @CapabilityState int mSetManualTimeZoneCapability; + private @CapabilityState int mConfigureNotificationsEnabledCapability; public Builder(@NonNull UserHandle userHandle) { mUserHandle = Objects.requireNonNull(userHandle); @@ -240,12 +271,14 @@ public final class TimeZoneCapabilities implements Parcelable { Objects.requireNonNull(capabilitiesToCopy); mUserHandle = capabilitiesToCopy.mUserHandle; mConfigureAutoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; mUseLocationEnabled = capabilitiesToCopy.mUseLocationEnabled; mConfigureGeoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; mSetManualTimeZoneCapability = - capabilitiesToCopy.mSetManualTimeZoneCapability; + capabilitiesToCopy.mSetManualTimeZoneCapability; + mConfigureNotificationsEnabledCapability = + capabilitiesToCopy.mConfigureNotificationsEnabledCapability; } /** Sets the value for the "configure automatic time zone detection enabled" capability. */ @@ -274,6 +307,14 @@ public final class TimeZoneCapabilities implements Parcelable { return this; } + /** + * Sets the value for the "configure time notifications enabled" capability. + */ + public Builder setConfigureNotificationsEnabledCapability(@CapabilityState int value) { + this.mConfigureNotificationsEnabledCapability = value; + return this; + } + /** Returns the {@link TimeZoneCapabilities}. */ @NonNull public TimeZoneCapabilities build() { @@ -283,7 +324,9 @@ public final class TimeZoneCapabilities implements Parcelable { verifyCapabilitySet(mConfigureGeoDetectionEnabledCapability, "configureGeoDetectionEnabledCapability"); verifyCapabilitySet(mSetManualTimeZoneCapability, - "mSetManualTimeZoneCapability"); + "setManualTimeZoneCapability"); + verifyCapabilitySet(mConfigureNotificationsEnabledCapability, + "configureNotificationsEnabledCapability"); return new TimeZoneCapabilities(this); } diff --git a/core/java/android/app/time/TimeZoneConfiguration.java b/core/java/android/app/time/TimeZoneConfiguration.java index 7403c129f4dc..68c090f6dde3 100644 --- a/core/java/android/app/time/TimeZoneConfiguration.java +++ b/core/java/android/app/time/TimeZoneConfiguration.java @@ -62,7 +62,8 @@ public final class TimeZoneConfiguration implements Parcelable { * * @hide */ - @StringDef({ SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED }) + @StringDef({SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED, + SETTING_NOTIFICATIONS_ENABLED}) @Retention(RetentionPolicy.SOURCE) @interface Setting {} @@ -74,6 +75,10 @@ public final class TimeZoneConfiguration implements Parcelable { @Setting private static final String SETTING_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; + /** See {@link TimeZoneConfiguration#areNotificationsEnabled()} for details. */ + @Setting + private static final String SETTING_NOTIFICATIONS_ENABLED = "notificationsEnabled"; + @NonNull private final Bundle mBundle; private TimeZoneConfiguration(Builder builder) { @@ -98,7 +103,8 @@ public final class TimeZoneConfiguration implements Parcelable { */ public boolean isComplete() { return hasIsAutoDetectionEnabled() - && hasIsGeoDetectionEnabled(); + && hasIsGeoDetectionEnabled() + && hasIsNotificationsEnabled(); } /** @@ -128,8 +134,7 @@ public final class TimeZoneConfiguration implements Parcelable { /** * Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This * controls whether the device can use geolocation to determine time zone. This value may only - * be used by Android under some circumstances. For example, it is not used when - * {@link #isGeoDetectionEnabled()} is {@code false}. + * be used by Android under some circumstances. * * <p>See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabledCapability()} for how to * tell if the setting is meaningful for the current user at this time. @@ -150,6 +155,32 @@ public final class TimeZoneConfiguration implements Parcelable { return mBundle.containsKey(SETTING_GEO_DETECTION_ENABLED); } + /** + * Returns the value of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. This controls + * whether the device can send time and time zone related notifications. This value may only + * be used by Android under some circumstances. + * + * <p>See {@link TimeZoneCapabilities#getConfigureNotificationsEnabledCapability()} ()} for how + * to tell if the setting is meaningful for the current user at this time. + * + * @throws IllegalStateException if the setting is not present + * + * @hide + */ + public boolean areNotificationsEnabled() { + enforceSettingPresent(SETTING_NOTIFICATIONS_ENABLED); + return mBundle.getBoolean(SETTING_NOTIFICATIONS_ENABLED); + } + + /** + * Returns {@code true} if the {@link #areNotificationsEnabled()} setting is present. + * + * @hide + */ + public boolean hasIsNotificationsEnabled() { + return mBundle.containsKey(SETTING_NOTIFICATIONS_ENABLED); + } + @Override public int describeContents() { return 0; @@ -244,6 +275,17 @@ public final class TimeZoneConfiguration implements Parcelable { return this; } + /** + * Sets the state of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. * + * + * @hide + */ + @NonNull + public Builder setNotificationsEnabled(boolean enabled) { + this.mBundle.putBoolean(SETTING_NOTIFICATIONS_ENABLED, enabled); + return this; + } + /** Returns the {@link TimeZoneConfiguration}. */ @NonNull public TimeZoneConfiguration build() { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e3e10388754c..350048df3112 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -12394,14 +12394,30 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public void collectExtraIntentKeys() { + collectExtraIntentKeys(false); + } + + /** + * Collects keys in the extra bundle whose value are intents. + * With these keys collected on the client side, the system server would only unparcel values + * of these keys and create IntentCreatorToken for them. + * This method could also be called from the system server side as a catch all safty net in case + * these keys are not collected on the client side. In that case, call it with forceUnparcel set + * to true since everything is parceled on the system server side. + * + * @param forceUnparcel if it is true, unparcel everything to determine if an object is an + * intent. Otherwise, do not unparcel anything. + * @hide + */ + public void collectExtraIntentKeys(boolean forceUnparcel) { if (preventIntentRedirect()) { - collectNestedIntentKeysRecur(new ArraySet<>()); + collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel); } } - private void collectNestedIntentKeysRecur(Set<Intent> visited) { + private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) { addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); - if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) { + if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) { for (String key : mExtras.keySet()) { Object value; try { @@ -12410,23 +12426,25 @@ public class Intent implements Parcelable, Cloneable { // It is okay to not collect a parceled intent since it would have been // coming from another process and collected by its containing intent already // in that process. - if (!mExtras.isValueParceled(key)) { + if (forceUnparcel || !mExtras.isValueParceled(key)) { value = mExtras.get(key); } else { value = null; } } catch (BadParcelableException e) { - // This probably would never happen. But just in case, simply ignore it since - // it is not an intent anyway. + // This may still happen if the keys are collected on the system server side, in + // which case, we will try to unparcel everything. If this happens, simply + // ignore it since it is not an intent anyway. value = null; } if (value instanceof Intent intent) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0), + forceUnparcel); } else if (value instanceof Parcelable[] parcelables) { - handleParcelableArray(parcelables, key, visited); + handleParcelableArray(parcelables, key, visited, forceUnparcel); } else if (value instanceof ArrayList<?> parcelables) { - handleParcelableList(parcelables, key, visited); + handleParcelableList(parcelables, key, visited, forceUnparcel); } } } @@ -12436,13 +12454,15 @@ public class Intent implements Parcelable, Cloneable { Intent intent = mClipData.getItemAt(i).mIntent; if (intent != null && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i), + forceUnparcel); } } } } - private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) { + private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key, + boolean forceUnparcel) { if (mCreatorTokenInfo == null) { mCreatorTokenInfo = new CreatorTokenInfo(); } @@ -12452,24 +12472,28 @@ public class Intent implements Parcelable, Cloneable { mCreatorTokenInfo.mNestedIntentKeys.add(key); if (!visited.contains(intent)) { visited.add(intent); - intent.collectNestedIntentKeysRecur(visited); + intent.collectNestedIntentKeysRecur(visited, forceUnparcel); } } - private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) { + private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.length; i++) { if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i), + forceUnparcel); } } } - private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) { + private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.size(); i++) { if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i), + forceUnparcel); } } } diff --git a/core/java/android/content/pm/SigningInfo.aidl b/core/java/android/content/pm/SigningInfo.aidl new file mode 100644 index 000000000000..bc986d1b214b --- /dev/null +++ b/core/java/android/content/pm/SigningInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +package android.content.pm; + +parcelable SigningInfo;
\ No newline at end of file diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index b938aac811fd..f538e9ffffdd 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -367,7 +367,7 @@ public final class ApkAssets { /** @hide */ public @NonNull String getDebugName() { synchronized (this) { - return nativeGetDebugName(mNativePtr); + return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr); } } diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 71702d996883..25cdc508fdce 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -137,6 +137,8 @@ public class HubEndpoint { serviceDescriptor, mLifecycleCallback.onSessionOpenRequest( initiator, serviceDescriptor))); + } else { + invokeCallbackFinished(); } } @@ -163,6 +165,8 @@ public class HubEndpoint { + result.getReason()); rejectSession(sessionId); } + + invokeCallbackFinished(); } private void acceptSession( @@ -249,7 +253,12 @@ public class HubEndpoint { activeSession.setOpened(); if (mLifecycleCallback != null) { mLifecycleCallbackExecutor.execute( - () -> mLifecycleCallback.onSessionOpened(activeSession)); + () -> { + mLifecycleCallback.onSessionOpened(activeSession); + invokeCallbackFinished(); + }); + } else { + invokeCallbackFinished(); } } @@ -278,7 +287,10 @@ public class HubEndpoint { synchronized (mLock) { mActiveSessions.remove(sessionId); } + invokeCallbackFinished(); }); + } else { + invokeCallbackFinished(); } } @@ -323,8 +335,17 @@ public class HubEndpoint { e.rethrowFromSystemServer(); } } + invokeCallbackFinished(); }); } + + private void invokeCallbackFinished() { + try { + mServiceToken.onCallbackFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } }; /** Binder returned from system service, non-null while registered. */ diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index 44f80c819e83..eb1255c06094 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -94,4 +94,10 @@ interface IContextHubEndpoint { */ @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode); + + /** + * Invoked when a callback from IContextHubEndpointCallback finishes. + */ + @EnforcePermission("ACCESS_CONTEXT_HUB") + void onCallbackFinished(); } diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java index f41550f6061e..75c652c973e4 100644 --- a/core/java/android/hardware/input/InputGestureData.java +++ b/core/java/android/hardware/input/InputGestureData.java @@ -48,27 +48,7 @@ public final class InputGestureData { /** Returns the trigger information for this input gesture */ public Trigger getTrigger() { - switch (mInputGestureData.trigger.getTag()) { - case AidlInputGestureData.Trigger.Tag.key: { - AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey(); - if (trigger == null) { - throw new RuntimeException("InputGestureData is corrupted, null key trigger!"); - } - return createKeyTrigger(trigger.keycode, trigger.modifierState); - } - case AidlInputGestureData.Trigger.Tag.touchpadGesture: { - AidlInputGestureData.TouchpadGestureTrigger trigger = - mInputGestureData.trigger.getTouchpadGesture(); - if (trigger == null) { - throw new RuntimeException( - "InputGestureData is corrupted, null touchpad trigger!"); - } - return createTouchpadTrigger(trigger.gestureType); - } - default: - throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!"); - - } + return createTriggerFromAidlTrigger(mInputGestureData.trigger); } /** Returns the action to perform for this input gesture */ @@ -147,18 +127,7 @@ public final class InputGestureData { "No app launch data for system action launch application"); } AidlInputGestureData data = new AidlInputGestureData(); - data.trigger = new AidlInputGestureData.Trigger(); - if (mTrigger instanceof KeyTrigger keyTrigger) { - data.trigger.setKey(new AidlInputGestureData.KeyTrigger()); - data.trigger.getKey().keycode = keyTrigger.getKeycode(); - data.trigger.getKey().modifierState = keyTrigger.getModifierState(); - } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) { - data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger()); - data.trigger.getTouchpadGesture().gestureType = - touchpadTrigger.getTouchpadGestureType(); - } else { - throw new IllegalArgumentException("Invalid trigger type!"); - } + data.trigger = mTrigger.getAidlTrigger(); data.gestureType = mKeyGestureType; if (mAppLaunchData != null) { if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) { @@ -198,6 +167,7 @@ public final class InputGestureData { } public interface Trigger { + AidlInputGestureData.Trigger getAidlTrigger(); } /** Creates a input gesture trigger based on a key press */ @@ -210,85 +180,128 @@ public final class InputGestureData { return new TouchpadTrigger(touchpadGestureType); } + public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) { + switch (aidlTrigger.getTag()) { + case AidlInputGestureData.Trigger.Tag.key: { + AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey(); + if (trigger == null) { + throw new RuntimeException("aidlTrigger is corrupted, null key trigger!"); + } + return new KeyTrigger(trigger); + } + case AidlInputGestureData.Trigger.Tag.touchpadGesture: { + AidlInputGestureData.TouchpadGestureTrigger trigger = + aidlTrigger.getTouchpadGesture(); + if (trigger == null) { + throw new RuntimeException( + "aidlTrigger is corrupted, null touchpad trigger!"); + } + return new TouchpadTrigger(trigger); + } + default: + throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!"); + + } + } + /** Key based input gesture trigger */ public static class KeyTrigger implements Trigger { - private static final int SHORTCUT_META_MASK = - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON - | KeyEvent.META_SHIFT_ON; - private final int mKeycode; - private final int mModifierState; + + AidlInputGestureData.KeyTrigger mAidlKeyTrigger; + + private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) { + mAidlKeyTrigger = aidlKeyTrigger; + } private KeyTrigger(int keycode, int modifierState) { if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) { throw new IllegalArgumentException("Invalid keycode = " + keycode); } - mKeycode = keycode; - mModifierState = modifierState; + mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger(); + mAidlKeyTrigger.keycode = keycode; + mAidlKeyTrigger.modifierState = modifierState; } public int getKeycode() { - return mKeycode; + return mAidlKeyTrigger.keycode; } public int getModifierState() { - return mModifierState; + return mAidlKeyTrigger.modifierState; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setKey(mAidlKeyTrigger); + return trigger; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof KeyTrigger that)) return false; - return mKeycode == that.mKeycode && mModifierState == that.mModifierState; + return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger); } @Override public int hashCode() { - return Objects.hash(mKeycode, mModifierState); + return mAidlKeyTrigger.hashCode(); } @Override public String toString() { return "KeyTrigger{" + - "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) + - ", mModifierState=" + mModifierState + + "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) + + ", mModifierState=" + mAidlKeyTrigger.modifierState + '}'; } } /** Touchpad based input gesture trigger */ public static class TouchpadTrigger implements Trigger { - private final int mTouchpadGestureType; + AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger; + + private TouchpadTrigger( + @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) { + mAidlTouchpadTrigger = aidlTouchpadTrigger; + } private TouchpadTrigger(int touchpadGestureType) { if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) { throw new IllegalArgumentException( "Invalid touchpadGestureType = " + touchpadGestureType); } - mTouchpadGestureType = touchpadGestureType; + mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger(); + mAidlTouchpadTrigger.gestureType = touchpadGestureType; } public int getTouchpadGestureType() { - return mTouchpadGestureType; + return mAidlTouchpadTrigger.gestureType; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setTouchpadGesture(mAidlTouchpadTrigger); + return trigger; } @Override public String toString() { return "TouchpadTrigger{" + - "mTouchpadGestureType=" + mTouchpadGestureType + + "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TouchpadTrigger that = (TouchpadTrigger) o; - return mTouchpadGestureType == that.mTouchpadGestureType; + if (!(o instanceof TouchpadTrigger that)) return false; + return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger); } @Override public int hashCode() { - return Objects.hashCode(mTouchpadGestureType); + return mAidlTouchpadTrigger.hashCode(); } } diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 66d073fa791e..cb1e0161441f 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -129,6 +129,7 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT = 79; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP = 80; public static final int KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN = 81; + public static final int KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS = 82; public static final int FLAG_CANCELLED = 1; @@ -225,6 +226,7 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_MAGNIFICATION_PAN_RIGHT, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP, KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -807,6 +809,8 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_UP"; case KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN: return "KEY_GESTURE_TYPE_MAGNIFICATION_PAN_DOWN"; + case KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return "KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS"; default: return Integer.toHexString(value); } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 1801df048b3e..2a5666cbe83c 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -615,6 +615,7 @@ public final class PowerManager { WAKE_REASON_WAKE_KEY, WAKE_REASON_WAKE_MOTION, WAKE_REASON_HDMI, + WAKE_REASON_LID, WAKE_REASON_DISPLAY_GROUP_ADDED, WAKE_REASON_DISPLAY_GROUP_TURNED_ON, WAKE_REASON_UNFOLD_DEVICE, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 2656a7b58c8d..11dddfb24ad5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9319,6 +9319,16 @@ public final class Settings { "accessibility_autoclick_delay"; /** + * Integer setting specifying the autoclick cursor area size (the radius of the autoclick + * ring indicator) when {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set. + * + * @see #ACCESSIBILITY_AUTOCLICK_ENABLED + * @hide + */ + public static final String ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE = + "accessibility_autoclick_cursor_area_size"; + + /** * Whether or not larger size icons are used for the pointer of mouse/trackpad for * accessibility. * (0 = false, 1 = true) @@ -13318,6 +13328,16 @@ public final class Settings { public static final String AUTO_TIME_ZONE_EXPLICIT = "auto_time_zone_explicit"; /** + * Value to specify if the device should send notifications when {@link #AUTO_TIME_ZONE} is + * on and the device's time zone changes. + * + * <p>1=yes, 0=no. + * + * @hide + */ + public static final String TIME_ZONE_NOTIFICATIONS = "time_zone_notifications"; + + /** * URI for the car dock "in" event sound. * @hide */ diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index e254bf3e016f..d53b98c65f9a 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -77,7 +77,7 @@ public abstract class Layout { private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f; private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f; // since we're not using soft light yet, this needs to be much lower than the spec'd 0.8 - private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.5f; + private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f; /** @hide */ @IntDef(prefix = { "BREAK_STRATEGY_" }, value = { diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index fd57aec4180b..544f42b9acfa 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -148,6 +148,15 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_DELAY_DEFAULT = 600; + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT = 60; + + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_MIN = 20; + + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100; + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 20e3f6b93bd0..2911b0a6643d 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -464,7 +464,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { * Returns false if the legacy back behavior should be used. */ public boolean isOnBackInvokedCallbackEnabled() { - return isOnBackInvokedCallbackEnabled(mChecker.getContext()); + final Context hostContext = mChecker.getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + return isOnBackInvokedCallbackEnabled(hostContext); } /** @@ -695,7 +700,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public boolean checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback) { - if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext()) + final Context hostContext = getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(hostContext) && !(callback instanceof CompatOnBackInvokedCallback)) { Log.w(TAG, "OnBackInvokedCallback is not enabled for the application." @@ -720,7 +730,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return true; } - private Context getContext() { + @Nullable private Context getContext() { return mContext.get(); } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 4241ec776465..be0b4fea459c 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -478,10 +478,10 @@ flag { } flag { - name: "enable_multiple_desktops" + name: "enable_multiple_desktops_frontend" namespace: "lse_desktop_experience" - description: "Enable multiple desktop sessions for desktop windowing." - bug: "379158791" + description: "Enable multiple desktop sessions for desktop windowing (frontend)." + bug: "362720309" } flag { diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java index 0b1ecf78d28c..d03bb5c3cb17 100644 --- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java +++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; @@ -351,4 +352,24 @@ public final class AccessibilityUtils { } return result; } + + /** Returns the {@link ComponentName} of an installed accessibility service by label. */ + @Nullable + public static ComponentName getInstalledAccessibilityServiceComponentNameByLabel( + Context context, String label) { + AccessibilityManager accessibilityManager = + context.getSystemService(AccessibilityManager.class); + List<AccessibilityServiceInfo> serviceInfos = + accessibilityManager.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo service : serviceInfos) { + final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; + if (label.equals(serviceInfo.loadLabel(context.getPackageManager()).toString()) + && (serviceInfo.applicationInfo.isSystemApp() + || serviceInfo.applicationInfo.isUpdatedSystemApp())) { + return new ComponentName(serviceInfo.packageName, serviceInfo.name); + } + } + return null; + } } diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 4aebde536dcf..972c2ea403e0 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -49,6 +49,7 @@ public class SystemNotificationChannels { public static final String NETWORK_ALERTS = "NETWORK_ALERTS"; public static final String NETWORK_AVAILABLE = "NETWORK_AVAILABLE"; public static final String VPN = "VPN"; + public static final String TIME = "TIME"; /** * @deprecated Legacy device admin channel with low importance which is no longer used, * Use the high importance {@link #DEVICE_ADMIN} channel instead. @@ -67,6 +68,7 @@ public class SystemNotificationChannels { @Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES"; public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS"; public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION"; + public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE"; public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY"; public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS"; @@ -145,6 +147,12 @@ public class SystemNotificationChannels { NotificationManager.IMPORTANCE_LOW); channelsList.add(vpn); + final NotificationChannel time = new NotificationChannel( + TIME, + context.getString(R.string.notification_channel_system_time), + NotificationManager.IMPORTANCE_DEFAULT); + channelsList.add(time); + final NotificationChannel deviceAdmin = new NotificationChannel( DEVICE_ADMIN, getDeviceAdminNotificationChannelName(context), @@ -203,6 +211,13 @@ public class SystemNotificationChannels { newFeaturePrompt.setBlockable(true); channelsList.add(newFeaturePrompt); + final NotificationChannel accessibilityHearingDeviceChannel = new NotificationChannel( + ACCESSIBILITY_HEARING_DEVICE, + context.getString(R.string.notification_channel_accessibility_hearing_device), + NotificationManager.IMPORTANCE_HIGH); + accessibilityHearingDeviceChannel.setBlockable(true); + channelsList.add(accessibilityHearingDeviceChannel); + final NotificationChannel accessibilitySecurityPolicyChannel = new NotificationChannel( ACCESSIBILITY_SECURITY_POLICY, context.getString(R.string.notification_channel_accessibility_security_policy), diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 8cd7843fe1d9..f0b54937546b 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.RemotableViewMethod; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -40,14 +42,12 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -56,18 +56,25 @@ import java.util.TreeSet; * represent Notification ProgressStyle progress, such as for ridesharing and navigation. */ @RemoteViews.RemoteView -public final class NotificationProgressBar extends ProgressBar { +public final class NotificationProgressBar extends ProgressBar implements + NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; private NotificationProgressDrawable mNotificationProgressDrawable; + private final Rect mProgressDrawableBounds = new Rect(); private NotificationProgressModel mProgressModel; @Nullable - private List<Part> mProgressDrawableParts = null; + private List<Part> mParts = null; + + // List of drawable parts before segment splitting by process. + @Nullable + private List<NotificationProgressDrawable.Part> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; + private boolean mHasTrackerIcon = false; /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; @@ -76,7 +83,13 @@ public final class NotificationProgressBar extends ProgressBar { private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; - private float mScale = 0; + private float mProgressFraction = 0; + /** + * The location of progress on the stretched and rescaled progress bar, in fraction. Used for + * calculating the tracker position. If stretching and rescaling is not needed, == + * mProgressFraction. + */ + private float mAdjustedProgressFraction = 0; /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ private boolean mTrackerPosIsDirty = false; @@ -104,12 +117,13 @@ public final class NotificationProgressBar extends ProgressBar { try { mNotificationProgressDrawable = getNotificationProgressDrawable(); + mNotificationProgressDrawable.setBoundsChangeListener(this); } catch (IllegalStateException ex) { Log.e(TAG, "Can't get NotificationProgressDrawable", ex); } // Supports setting the tracker in xml, but ProgressStyle notifications set/override it - // via {@code setProgressTrackerIcon}. + // via {@code #setProgressTrackerIcon}. final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); setTracker(tracker); @@ -137,20 +151,25 @@ public final class NotificationProgressBar extends ProgressBar { final int indeterminateColor = mProgressModel.getIndeterminateColor(); setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); } else { + // TODO: b/372908709 - maybe don't rerun the entire calculation every time the + // progress model is updated? For example, if the segments and parts aren't changed, + // there is no need to call `processAndConvertToViewParts` again. + final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); - mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(), + + mParts = processAndConvertToViewParts(mProgressModel.getSegments(), mProgressModel.getPoints(), progress, - progressMax, - mProgressModel.isStyledByProgress()); - - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setParts(mProgressDrawableParts); - } + progressMax); setMax(progressMax); setProgress(progress); + + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0) { + updateDrawableParts(); + } } } @@ -200,9 +219,7 @@ public final class NotificationProgressBar extends ProgressBar { } else { progressTrackerDrawable = null; } - return () -> { - setTracker(progressTrackerDrawable); - }; + return () -> setTracker(progressTrackerDrawable); } private void setTracker(@Nullable Drawable tracker) { @@ -226,8 +243,14 @@ public final class NotificationProgressBar extends ProgressBar { final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); mTracker = tracker; - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null); + final boolean hasTrackerIcon = (mTracker != null); + if (mHasTrackerIcon != hasTrackerIcon) { + mHasTrackerIcon = hasTrackerIcon; + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0 + && mProgressModel.isStyledByProgress()) { + updateDrawableParts(); + } } configureTrackerBounds(); @@ -293,6 +316,8 @@ public final class NotificationProgressBar extends ProgressBar { mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setProgress(int progress) { super.setProgress(progress); @@ -300,6 +325,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public void setProgress(int progress, boolean animate) { // Animation isn't supported by NotificationProgressBar. @@ -308,6 +335,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMin(int min) { super.setMin(min); @@ -315,6 +344,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMax(int max) { super.setMax(max); @@ -323,10 +354,10 @@ public final class NotificationProgressBar extends ProgressBar { } private void onMaybeVisualProgressChanged() { - float scale = getScale(); - if (mScale == scale) return; + float progressFraction = getProgressFraction(); + if (mProgressFraction == progressFraction) return; - mScale = scale; + mProgressFraction = progressFraction; mTrackerPosIsDirty = true; invalidate(); } @@ -372,6 +403,59 @@ public final class NotificationProgressBar extends ProgressBar { updateTrackerAndBarPos(w, h); } + @Override + public void onDrawableBoundsChanged() { + final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); + + if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; + + if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { + updateDrawableParts(); + } + + mProgressDrawableBounds.set(progressDrawableBounds); + } + + private void updateDrawableParts() { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + + if (mNotificationProgressDrawable == null) return; + if (mParts == null) return; + + final float width = mNotificationProgressDrawable.getBounds().width(); + if (width == 0) { + if (mProgressDrawableParts != null) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + mProgressDrawableParts.clear(); + mNotificationProgressDrawable.setParts(mProgressDrawableParts); + } + return; + } + + mProgressDrawableParts = processAndConvertToDrawableParts( + mParts, + width, + mNotificationProgressDrawable.getSegSegGap(), + mNotificationProgressDrawable.getSegPointGap(), + mNotificationProgressDrawable.getPointRadius(), + mHasTrackerIcon + ); + Pair<List<NotificationProgressDrawable.Part>, Float> p = maybeStretchAndRescaleSegments( + mParts, + mProgressDrawableParts, + mNotificationProgressDrawable.getSegmentMinWidth(), + mNotificationProgressDrawable.getPointRadius(), + getProgressFraction(), + width, + mProgressModel.isStyledByProgress(), + mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); + + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + mNotificationProgressDrawable.setParts(p.first); + mAdjustedProgressFraction = p.second / width; + } + private void updateTrackerAndBarPos(int w, int h) { final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable bar = getCurrentDrawable(); @@ -402,11 +486,11 @@ public final class NotificationProgressBar extends ProgressBar { } if (tracker != null) { - setTrackerPos(w, tracker, mScale, trackerOffsetY); + setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); } } - private float getScale() { + private float getProgressFraction() { int min = getMin(); int max = getMax(); int range = max - min; @@ -418,17 +502,17 @@ public final class NotificationProgressBar extends ProgressBar { * * @param w Width of the view, including padding * @param tracker Drawable used for the tracker - * @param scale Current progress between 0 and 1 + * @param progressFraction Current progress between 0 and 1 * @param offsetY Vertical offset for centering. If set to * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) { + private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); - final int trackerPos = (int) (scale * available + 0.5f); + final int trackerPos = (int) (progressFraction * available + 0.5f); final int top, bottom; if (offsetY == Integer.MIN_VALUE) { @@ -482,7 +566,7 @@ public final class NotificationProgressBar extends ProgressBar { if (mTracker == null) return; if (mTrackerPosIsDirty) { - setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); } final int saveCount = canvas.save(); @@ -531,7 +615,7 @@ public final class NotificationProgressBar extends ProgressBar { final Drawable tracker = mTracker; if (tracker != null) { - setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); // Since we draw translated, the drawable's bounds that it signals // for invalidation won't be the actual bounds we want invalidated, @@ -541,16 +625,14 @@ public final class NotificationProgressBar extends ProgressBar { } /** - * Processes the ProgressStyle data and convert to list of {@code - * NotificationProgressDrawable.Part}. + * Processes the ProgressStyle data and convert to a list of {@code Part}. */ @VisibleForTesting - public static List<Part> processAndConvertToDrawableParts( + public static List<Part> processAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { if (segments.isEmpty()) { throw new IllegalArgumentException("List of segments shouldn't be empty"); @@ -571,6 +653,7 @@ public final class NotificationProgressBar extends ProgressBar { if (progress < 0 || progress > progressMax) { throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -583,23 +666,21 @@ public final class NotificationProgressBar extends ProgressBar { final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( points); final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, - positionToPointMap, progress, isStyledByProgress); + positionToPointMap); final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPointsAndProgress( - startToSegmentMap, sortedPos, progressMax); + splitSegmentsByPoints(startToSegmentMap, sortedPos, progressMax); - return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos, - progress, progressMax, - isStyledByProgress); + return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, + progressMax); } // Any segment with a point on it gets split by the point. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress( + private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, - int progressMax) { + int progressMax + ) { int prevSegStart = 0; for (Integer pos : sortedPos) { if (pos == 0 || pos == progressMax) continue; @@ -624,32 +705,22 @@ public final class NotificationProgressBar extends ProgressBar { return startToSegmentMap; } - private static List<Part> convertToDrawableParts( + private static List<Part> convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, - int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { List<Part> parts = new ArrayList<>(); - boolean styleRemainingParts = false; for (Integer pos : sortedPos) { if (positionToPointMap.containsKey(pos)) { final ProgressStyle.Point point = positionToPointMap.get(pos); - final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts); - parts.add(new Point(null, color, styleRemainingParts)); - } - // We want the Point at the current progress to be filled (not faded), but a Segment - // starting at this progress to be faded. - if (isStyledByProgress && !styleRemainingParts && pos == progress) { - styleRemainingParts = true; + parts.add(new Point(point.getColor())); } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts); parts.add(new Segment( - (float) seg.getLength() / progressMax, color, styleRemainingParts)); + (float) seg.getLength() / progressMax, seg.getColor())); } } @@ -660,11 +731,24 @@ public final class NotificationProgressBar extends ProgressBar { private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { if (!fade) return color; - return NotificationProgressDrawable.getFadedColor(color); + return getFadedColor(color); + } + + /** + * Get a color with an opacity that's 40% of the input color. + */ + @ColorInt + static int getFadedColor(@ColorInt int color) { + return Color.argb( + (int) (Color.alpha(color) * 0.4f + 0.5f), + Color.red(color), + Color.green(color), + Color.blue(color)); } private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( - List<ProgressStyle.Segment> segments) { + List<ProgressStyle.Segment> segments + ) { final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); int currentStart = 0; // Initial start position is 0 @@ -681,7 +765,8 @@ public final class NotificationProgressBar extends ProgressBar { } private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( - List<ProgressStyle.Point> points) { + List<ProgressStyle.Point> points + ) { final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); for (ProgressStyle.Point point : points) { @@ -693,14 +778,404 @@ public final class NotificationProgressBar extends ProgressBar { private static SortedSet<Integer> generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, - Map<Integer, ProgressStyle.Point> positionToPointMap, int progress, - boolean isStyledByProgress) { + Map<Integer, ProgressStyle.Point> positionToPointMap + ) { final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); sortedPos.addAll(positionToPointMap.keySet()); - if (isStyledByProgress) { - sortedPos.add(progress); - } return sortedPos; } + + /** + * Processes the list of {@code Part} and convert to a list of + * {@code NotificationProgressDrawable.Part}. + */ + @VisibleForTesting + public static List<NotificationProgressDrawable.Part> processAndConvertToDrawableParts( + List<Part> parts, + float totalWidth, + float segSegGap, + float segPointGap, + float pointRadius, + boolean hasTrackerIcon + ) { + List<NotificationProgressDrawable.Part> drawableParts = new ArrayList<>(); + + // generally, we will start drawing at (x, y) and end at (x+w, y) + float x = (float) 0; + + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); + final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); + if (part instanceof Segment segment) { + final float segWidth = segment.mFraction * totalWidth; + // Advance the start position to account for a point immediately prior. + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); + final float start = x + startOffset; + // Retract the end position to account for the padding and a point immediately + // after. + final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, + segSegGap, x + segWidth, totalWidth, hasTrackerIcon); + final float end = x + segWidth - endOffset; + + drawableParts.add( + new NotificationProgressDrawable.Segment(start, end, segment.mColor, + segment.mFaded)); + + segment.mStart = x; + segment.mEnd = x + segWidth; + + // Advance the current position to account for the segment's fraction of the total + // width (ignoring offset and padding) + x += segWidth; + } else if (part instanceof Point point) { + final float pointWidth = 2 * pointRadius; + float start = x - pointRadius; + if (start < 0) start = 0; + float end = start + pointWidth; + if (end > totalWidth) { + end = totalWidth; + if (totalWidth > pointWidth) start = totalWidth - pointWidth; + } + + drawableParts.add( + new NotificationProgressDrawable.Point(start, end, point.mColor)); + } + } + + return drawableParts; + } + + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, + float startX) { + if (!(prevPart instanceof Point)) return 0F; + final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; + return pointOffset + pointRadius + segPointGap; + } + + private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, + float segPointGap, + float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { + if (nextPart == null) return 0F; + if (nextPart instanceof Segment nextSeg) { + if (!seg.mFaded && nextSeg.mFaded) { + // @see Segment#mFaded + return hasTrackerIcon ? 0F : segSegGap; + } + return segSegGap; + } + + final float pointWidth = 2 * pointRadius; + final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) + ? (endX + pointRadius - totalWidth) : 0; + return segPointGap + pointRadius + pointOffset; + } + + /** + * Processes the list of {@code NotificationProgressBar.Part} data and convert to a pair of: + * - list of {@code NotificationProgressDrawable.Part}. + * - location of progress on the stretched and rescaled progress bar. + */ + @VisibleForTesting + public static Pair<List<NotificationProgressDrawable.Part>, Float> + maybeStretchAndRescaleSegments( + List<Part> parts, + List<NotificationProgressDrawable.Part> drawableParts, + float segmentMinWidth, + float pointRadius, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + final List<NotificationProgressDrawable.Segment> drawableSegments = drawableParts + .stream() + .filter(NotificationProgressDrawable.Segment.class::isInstance) + .map(NotificationProgressDrawable.Segment.class::cast) + .toList(); + float totalExcessWidth = 0; + float totalPositiveExcessWidth = 0; + for (NotificationProgressDrawable.Segment drawableSegment : drawableSegments) { + final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; + totalExcessWidth += excessWidth; + if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; + } + + // All drawable segments are above minimum width. No need to stretch and rescale. + if (totalExcessWidth == totalPositiveExcessWidth) { + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + if (totalExcessWidth < 0) { + // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback + // option. (instead of return.) + Log.w(TAG, "Not enough width to satisfy the minimum width for segments."); + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + final int nParts = drawableParts.size(); + float startOffset = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + final float origDrawableSegmentWidth = drawableSegment.getWidth(); + + float drawableSegmentWidth = segmentMinWidth; + // Allocate the totalExcessWidth to the segments above minimum, proportionally to + // their initial excessWidth. + if (origDrawableSegmentWidth > segmentMinWidth) { + drawableSegmentWidth += + totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) + / totalPositiveExcessWidth; + } + + final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); + + // Adjust drawable segments to new widths + drawableSegment.setStart(drawableSegment.getStart() + startOffset); + drawableSegment.setEnd( + drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); + + // Also adjust view segments to new width. (For view segments, only start is + // needed?) + // Check that segments and drawableSegments are of the same size? + final Segment segment = (Segment) parts.get(iPart); + final float origSegmentWidth = segment.getWidth(); + segment.mStart = segment.mStart + startOffset; + segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; + + // Increase startOffset for the subsequent segments. + startOffset += widthDiff; + } else if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + drawablePoint.setStart(drawablePoint.getStart() + startOffset); + drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); + } + } + + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + // Find the location of progress on the stretched and rescaled progress bar. + // If isStyledByProgress is true, also split the segment with the progress value in its range. + private static Pair<List<NotificationProgressDrawable.Part>, Float> + maybeSplitDrawableSegmentsByProgress( + // Needed to get the original segment start and end positions in pixels. + List<Part> parts, + List<NotificationProgressDrawable.Part> drawableParts, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + + int iPartFirstSegmentToStyle = -1; + int iPartSegmentToSplit = -1; + float rescaledProgressX = 0; + float startFraction = 0; + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + if (!(part instanceof Segment)) continue; + final Segment segment = (Segment) part; + if (startFraction == progressFraction) { + iPartFirstSegmentToStyle = iPart; + rescaledProgressX = segment.mStart; + break; + } else if (startFraction < progressFraction + && progressFraction < startFraction + segment.mFraction) { + iPartSegmentToSplit = iPart; + rescaledProgressX = + segment.mStart + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); + break; + } + startFraction += segment.mFraction; + } + + if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); + + List<NotificationProgressDrawable.Part> splitDrawableParts = new ArrayList<>(); + boolean styleRemainingParts = false; + for (int iPart = 0; iPart < nParts; iPart++) { + final NotificationProgressDrawable.Part drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof NotificationProgressDrawable.Point drawablePoint) { + final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); + splitDrawableParts.add( + new NotificationProgressDrawable.Point(drawablePoint.getStart(), + drawablePoint.getEnd(), color)); + } + if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; + if (drawablePart instanceof NotificationProgressDrawable.Segment drawableSegment) { + if (iPart == iPartSegmentToSplit) { + if (rescaledProgressX <= drawableSegment.getStart()) { + styleRemainingParts = true; + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + color, true)); + } else if (drawableSegment.getStart() < rescaledProgressX + && rescaledProgressX < drawableSegment.getEnd()) { + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + rescaledProgressX - progressGap, + drawableSegment.getColor())); + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(rescaledProgressX, + drawableSegment.getEnd(), color, true)); + styleRemainingParts = true; + } else { + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + drawableSegment.getColor())); + styleRemainingParts = true; + } + } else { + final int color = maybeGetFadedColor(drawableSegment.getColor(), + styleRemainingParts); + splitDrawableParts.add( + new NotificationProgressDrawable.Segment(drawableSegment.getStart(), + drawableSegment.getEnd(), + color, styleRemainingParts)); + } + } + } + + return new Pair<>(splitDrawableParts, rescaledProgressX); + } + + /** + * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a + * {@link Point} with zero length. + */ + // TODO: b/372908709 - maybe this should be made private? Only test the final + // NotificationDrawable.Parts. + // TODO: b/372908709 - rename to BarPart, BarSegment, BarPoint. This avoids naming ambiguity + // with the types in NotificationProgressDrawable. + public interface Part { + } + + /** + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * + */ + public static final class Segment implements Part { + private final float mFraction; + @ColorInt private final int mColor; + /** Whether the segment is faded or not. + * <p> + * <pre> + * When mFaded is set to true, a combination of the following is done to the segment: + * 1. The drawing color is mColor with opacity updated to 40%. + * 2. The gap between faded and non-faded segments is: + * - the segment-segment gap, when there is no tracker icon + * - 0, when there is tracker icon + * </pre> + * </p> + */ + private final boolean mFaded; + + /** Start position (in pixels) */ + private float mStart; + /** End position (in pixels */ + private float mEnd; + + public Segment(float fraction, @ColorInt int color) { + this(fraction, color, false); + } + + public Segment(float fraction, @ColorInt int color, boolean faded) { + mFraction = fraction; + mColor = color; + mFaded = faded; + } + + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; + } + + @Override + public String toString() { + return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" + + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; + } + + // Needed for unit tests + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Segment that = (Segment) other; + if (Float.compare(this.mFraction, that.mFraction) != 0) return false; + if (this.mColor != that.mColor) return false; + return this.mFaded == that.mFaded; + } + + @Override + public int hashCode() { + return Objects.hash(mFraction, mColor, mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point implements Part { + @ColorInt private final int mColor; + + public Point(@ColorInt int color) { + mColor = color; + } + + @Override + public String toString() { + return "Point(color=" + this.mColor + ")"; + } + + // Needed for unit tests. + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Point that = (Point) other; + + return this.mColor == that.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mColor); + } + } } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 8629a1c95202..ef0a5d5cdec2 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -49,7 +48,8 @@ import java.util.Objects; /** * This is used by NotificationProgressBar for displaying a custom background. It composes of - * segments, which have non-zero length, and points, which have zero length. + * segments, which have non-zero length varying drawing width, and points, which have zero length + * and fixed size for drawing. * * @see Segment * @see Point @@ -57,14 +57,15 @@ import java.util.Objects; public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; + @Nullable + private BoundsChangeListener mBoundsChangeListener = null; + private State mState; private boolean mMutated; private final ArrayList<Part> mParts = new ArrayList<>(); - private boolean mHasTrackerIcon; private final RectF mSegRectF = new RectF(); - private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); private final Paint mFillPaint = new Paint(); @@ -80,27 +81,31 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * <p>Set the segment default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the stroke - * @see #mutate() + * Returns the gap between two segments. */ - public void setSegmentDefaultColor(@ColorInt int color) { - mState.setSegmentColor(color); + public float getSegSegGap() { + return mState.mSegSegGap; } /** - * <p>Set the point rect default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the point rect - * @see #mutate() + * Returns the gap between a segment and a point. + */ + public float getSegPointGap() { + return mState.mSegPointGap; + } + + /** + * Returns the gap between a segment and a point. */ - public void setPointRectDefaultColor(@ColorInt int color) { - mState.setPointRectColor(color); + public float getSegmentMinWidth() { + return mState.mSegmentMinWidth; + } + + /** + * Returns the radius for the points. + */ + public float getPointRadius() { + return mState.mPointRadius; } /** @@ -120,47 +125,18 @@ public final class NotificationProgressDrawable extends Drawable { setParts(Arrays.asList(parts)); } - /** - * Set whether a tracker is drawn on top of this NotificationProgressDrawable. - */ - public void setHasTrackerIcon(boolean hasTrackerIcon) { - if (mHasTrackerIcon != hasTrackerIcon) { - mHasTrackerIcon = hasTrackerIcon; - invalidateSelf(); - } - } - @Override public void draw(@NonNull Canvas canvas) { - final float pointRadius = - mState.mPointRadius; // how big the point icon will be, halved - - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) getBounds().left; + final float pointRadius = mState.mPointRadius; + final float left = (float) getBounds().left; final float centerY = (float) getBounds().centerY(); - final float totalWidth = (float) getBounds().width(); - float segPointGap = mState.mSegPointGap; final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { final Part part = mParts.get(iPart); - final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1); - final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1); + final float start = left + part.mStart; + final float end = left + part.mEnd; if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; - // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); - final float start = x + startOffset; - // Retract the end position to account for the padding and a point immediately - // after. - final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon); - final float end = x + segWidth - endOffset; - - // Advance the current position to account for the segment's fraction of the total - // width (ignoring offset and padding) - x += segWidth; - // No space left to draw the segment if (start > end) continue; @@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable { : mState.mSegmentHeight / 2F; final float cornerRadius = mState.mSegmentCornerRadius; - mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor)); + mFillPaint.setColor(segment.mColor); mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); } else if (part instanceof Point point) { - final float pointWidth = 2 * pointRadius; - float start = x - pointRadius; - if (start < 0) start = 0; - float end = start + pointWidth; - if (end > totalWidth) { - end = totalWidth; - if (totalWidth > pointWidth) start = totalWidth - pointWidth; - } - mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end, - (int) (centerY + pointRadius)); - - if (point.mIcon != null) { - point.mIcon.setBounds(mPointRect); - point.mIcon.draw(canvas); - } else { - // TODO: b/367804171 - actually use a vector asset for the default point - // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); - final float inset = mState.mPointRectInset; - final float cornerRadius = mState.mPointRectCornerRadius; - mPointRectF.inset(inset, inset); - - mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor - : (point.mFaded ? mState.mFadedPointRectColor - : mState.mPointRectColor)); - - canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); - } - } - } - } + // TODO: b/367804171 - actually use a vector asset for the default point + // rather than drawing it as a box? + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + final float inset = mState.mPointRectInset; + final float cornerRadius = mState.mPointRectCornerRadius; + mPointRectF.inset(inset, inset); - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - float startX) { - if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; - return pointOffset + pointRadius + segPointGap; - } + mFillPaint.setColor(point.mColor); - private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { - if (nextPart == null) return 0F; - if (nextPart instanceof Segment nextSeg) { - if (!seg.mFaded && nextSeg.mFaded) { - // @see Segment#mFaded - return hasTrackerIcon ? 0F : segSegGap; + canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); } - return segSegGap; } - - final float pointWidth = 2 * pointRadius; - final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) - ? (endX + pointRadius - totalWidth) : 0; - return segPointGap + pointRadius + pointOffset; } @Override @@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable { return PixelFormat.UNKNOWN; } + public void setBoundsChangeListener(BoundsChangeListener listener) { + mBoundsChangeListener = listener; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mBoundsChangeListener != null) { + mBoundsChangeListener.onDrawableBoundsChanged(); + } + } + @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) @@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable { // Extract the theme attributes, if any. state.mThemeAttrsSegments = a.extractThemeAttrs(); + state.mSegmentMinWidth = a.getDimension( + R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth); state.mSegmentHeight = a.getDimension( R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight); state.mFadedSegmentHeight = a.getDimension( @@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mSegmentCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawableSegments_cornerRadius, state.mSegmentCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color, - state.mSegmentColor); - setSegmentDefaultColor(color); } private void updatePointsFromTypedArray(TypedArray a) { @@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mPointRectCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawablePoints_cornerRadius, state.mPointRectCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color, - state.mPointRectColor); - setPointRectDefaultColor(color); } static int resolveDensity(@Nullable Resources r, int parentDensity) { @@ -464,63 +405,56 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a - * {@link Point} with zero length. + * Listener to receive updates about drawable bounds changing */ - public interface Part { + public interface BoundsChangeListener { + /** Called when bounds have changed */ + void onDrawableBoundsChanged(); } /** - * A segment is a part of the progress bar with non-zero length. For example, it can - * represent a portion in a navigation journey with certain traffic condition. - * + * A part of the progress bar, which is either a {@link Segment} with non-zero length and + * varying drawing width, or a {@link Point} with zero length and fixed size for drawing. */ - public static final class Segment implements Part { - private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. - * <p> - * <pre> - * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 40%. - * 2. The gap between faded and non-faded segments is: - * - the segment-segment gap, when there is no tracker icon - * - 0, when there is tracker icon - * </pre> - * </p> - */ - private final boolean mFaded; - - public Segment(float fraction) { - this(fraction, Color.TRANSPARENT); + public abstract static class Part { + // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the + // bounds rect. + /** Start position for drawing (in pixels) */ + protected float mStart; + /** End position for drawing (in pixels) */ + protected float mEnd; + /** Drawing color. */ + @ColorInt protected final int mColor; + + protected Part(float start, float end, @ColorInt int color) { + mStart = start; + mEnd = end; + mColor = color; } - public Segment(float fraction, @ColorInt int color) { - this(fraction, color, false); + public float getStart() { + return this.mStart; } - public Segment(float fraction, @ColorInt int color, boolean faded) { - mFraction = fraction; - mColor = color; - mFaded = faded; + public void setStart(float start) { + mStart = start; } - public float getFraction() { - return this.mFraction; + public float getEnd() { + return this.mEnd; } - public int getColor() { - return this.mColor; + public void setEnd(float end) { + mEnd = end; } - public boolean getFaded() { - return this.mFaded; + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; } - @Override - public String toString() { - return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" - + this.mFaded + ')'; + public int getColor() { + return this.mColor; } // Needed for unit tests @@ -530,80 +464,79 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Segment that = (Segment) other; - if (Float.compare(this.mFraction, that.mFraction) != 0) return false; - if (this.mColor != that.mColor) return false; - return this.mFaded == that.mFaded; + Part that = (Part) other; + if (Float.compare(this.mStart, that.mStart) != 0) return false; + if (Float.compare(this.mEnd, that.mEnd) != 0) return false; + return this.mColor == that.mColor; } @Override public int hashCode() { - return Objects.hash(mFraction, mColor, mFaded); + return Objects.hash(mStart, mEnd, mColor); } } /** - * A point is a part of the progress bar with zero length. Points are designated points within a - * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop - * ride-share journey. + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * <p> + * The start and end positions for drawing a segment are assumed to have been adjusted for + * the Points and gaps neighboring the segment. + * </p> */ - public static final class Point implements Part { - @Nullable - private final Drawable mIcon; - @ColorInt private final int mColor; + public static final class Segment extends Part { + /** + * Whether the segment is faded or not. + * <p> + * Faded segments and non-faded segments are drawn with different heights. + * </p> + */ private final boolean mFaded; - public Point(@Nullable Drawable icon) { - this(icon, Color.TRANSPARENT, false); - } - - public Point(@Nullable Drawable icon, @ColorInt int color) { - this(icon, color, false); - + public Segment(float start, float end, int color) { + this(start, end, color, false); } - public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) { - mIcon = icon; - mColor = color; + public Segment(float start, float end, int color, boolean faded) { + super(start, end, color); mFaded = faded; } - @Nullable - public Drawable getIcon() { - return this.mIcon; - } - - public int getColor() { - return this.mColor; - } - - public boolean getFaded() { - return this.mFaded; - } - @Override public String toString() { - return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded - + ")"; + return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ", faded=" + this.mFaded + ')'; } // Needed for unit tests. @Override public boolean equals(@Nullable Object other) { - if (this == other) return true; - - if (other == null || getClass() != other.getClass()) return false; - - Point that = (Point) other; + if (!super.equals(other)) return false; - if (!Objects.equals(this.mIcon, that.mIcon)) return false; - if (this.mColor != that.mColor) return false; + Segment that = (Segment) other; return this.mFaded == that.mFaded; } @Override public int hashCode() { - return Objects.hash(mIcon, mColor, mFaded); + return Objects.hash(super.hashCode(), mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point extends Part { + public Point(float start, float end, int color) { + super(start, end, color); + } + + @Override + public String toString() { + return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ")"; } } @@ -628,16 +561,14 @@ public final class NotificationProgressDrawable extends Drawable { int mChangingConfigurations; float mSegSegGap = 0.0f; float mSegPointGap = 0.0f; + float mSegmentMinWidth = 0.0f; float mSegmentHeight; float mFadedSegmentHeight; float mSegmentCornerRadius; - int mSegmentColor; - int mFadedSegmentColor; + // how big the point icon will be, halved float mPointRadius; float mPointRectInset; float mPointRectCornerRadius; - int mPointRectColor; - int mFadedPointRectColor; int[] mThemeAttrs; int[] mThemeAttrsSegments; @@ -652,16 +583,13 @@ public final class NotificationProgressDrawable extends Drawable { mChangingConfigurations = orig.mChangingConfigurations; mSegSegGap = orig.mSegSegGap; mSegPointGap = orig.mSegPointGap; + mSegmentMinWidth = orig.mSegmentMinWidth; mSegmentHeight = orig.mSegmentHeight; mFadedSegmentHeight = orig.mFadedSegmentHeight; mSegmentCornerRadius = orig.mSegmentCornerRadius; - mSegmentColor = orig.mSegmentColor; - mFadedSegmentColor = orig.mFadedSegmentColor; mPointRadius = orig.mPointRadius; mPointRectInset = orig.mPointRectInset; mPointRectCornerRadius = orig.mPointRectCornerRadius; - mPointRectColor = orig.mPointRectColor; - mFadedPointRectColor = orig.mFadedPointRectColor; mThemeAttrs = orig.mThemeAttrs; mThemeAttrsSegments = orig.mThemeAttrsSegments; @@ -674,6 +602,18 @@ public final class NotificationProgressDrawable extends Drawable { } private void applyDensityScaling(int sourceDensity, int targetDensity) { + if (mSegSegGap > 0) { + mSegSegGap = scaleFromDensity( + mSegSegGap, sourceDensity, targetDensity); + } + if (mSegPointGap > 0) { + mSegPointGap = scaleFromDensity( + mSegPointGap, sourceDensity, targetDensity); + } + if (mSegmentMinWidth > 0) { + mSegmentMinWidth = scaleFromDensity( + mSegmentMinWidth, sourceDensity, targetDensity); + } if (mSegmentHeight > 0) { mSegmentHeight = scaleFromDensity( mSegmentHeight, sourceDensity, targetDensity); @@ -740,28 +680,6 @@ public final class NotificationProgressDrawable extends Drawable { applyDensityScaling(sourceDensity, targetDensity); } } - - public void setSegmentColor(int color) { - mSegmentColor = color; - mFadedSegmentColor = getFadedColor(color); - } - - public void setPointRectColor(int color) { - mPointRectColor = color; - mFadedPointRectColor = getFadedColor(color); - } - } - - /** - * Get a color with an opacity that's 25% of the input color. - */ - @ColorInt - static int getFadedColor(@ColorInt int color) { - return Color.argb( - (int) (Color.alpha(color) * 0.4f + 0.5f), - Color.red(color), - Color.green(color), - Color.blue(color)); } @Override diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 96d34a0230b3..4f7ba9388a1d 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -107,6 +107,8 @@ message SecureSettingsProto { optional SettingProto accessibility_key_gesture_targets = 59 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto hct_rect_prompt_status = 60 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto em_value = 61 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Settings for accessibility autoclick + optional SettingProto autoclick_cursor_area_size = 62 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml index 5d272fb00e34..ff5450ee106f 100644 --- a/core/res/res/drawable/notification_progress.xml +++ b/core/res/res/drawable/notification_progress.xml @@ -24,6 +24,7 @@ android:segPointGap="@dimen/notification_progress_segPoint_gap"> <segments android:color="?attr/colorProgressBackgroundNormal" + android:minWidth="@dimen/notification_progress_segments_min_width" android:height="@dimen/notification_progress_segments_height" android:fadedHeight="@dimen/notification_progress_segments_faded_height" android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/> diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index db79e79c96df..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -136,10 +136,10 @@ <ImageView android:id="@+id/phishing_alert" - android:layout_width="@dimen/notification_phishing_alert_size" - android:layout_height="@dimen/notification_phishing_alert_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:src="@drawable/ic_dialog_alert_material" android:visibility="gone" @@ -148,10 +148,10 @@ <ImageView android:id="@+id/profile_badge" - android:layout_width="@dimen/notification_badge_size" - android:layout_height="@dimen/notification_badge_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:visibility="gone" android:contentDescription="@string/notification_work_profile_content_description" @@ -159,10 +159,10 @@ <ImageView android:id="@+id/alerted_icon" - android:layout_width="@dimen/notification_alerted_size" - android:layout_height="@dimen/notification_alerted_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:contentDescription="@string/notification_alerted_content_description" android:scaleType="fitCenter" android:src="@drawable/ic_notifications_alerted" diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index f108ce5bd1b9..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -87,7 +87,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -104,7 +104,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index bd17a3a0a74e..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -89,7 +89,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -106,7 +106,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index edbebb17f825..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -115,7 +115,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -132,7 +132,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index 0c07053d428a..72b3798e0780 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -68,7 +68,7 @@ android:theme="@style/Theme.DeviceDefault.Notification" > - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml index e4ff835a3524..084ec7daa683 100644 --- a/core/res/res/layout/notification_2025_template_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2014 The Android Open Source Project + ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. diff --git a/core/res/res/layout/notification_2025_top_line_views.xml b/core/res/res/layout/notification_2025_top_line_views.xml new file mode 100644 index 000000000000..74873463391e --- /dev/null +++ b/core/res/res/layout/notification_2025_top_line_views.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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 + --> +<!-- + This layout file should be included inside a NotificationTopLineView, sometimes after a + <TextView android:id="@+id/title"/> +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android"> + + <TextView + android:id="@+id/app_name_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:visibility="?attr/notificationHeaderAppNameVisibility" + /> + + <TextView + android:id="@+id/header_text_secondary_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/header_text_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/time_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:singleLine="true" + android:visibility="gone" + /> + + <DateTimeView + android:id="@+id/time" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + + <ViewStub + android:id="@+id/chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:layout="@layout/notification_template_part_chronometer" + android:visibility="gone" + /> + + <ImageButton + android:id="@+id/feedback" + android:layout_width="@dimen/notification_feedback_size" + android:layout_height="@dimen/notification_feedback_size" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:baseline="13dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_feedback_indicator" + android:background="?android:selectableItemBackgroundBorderless" + android:visibility="gone" + android:contentDescription="@string/notification_feedback_indicator" + /> + + <ImageView + android:id="@+id/phishing_alert" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:src="@drawable/ic_dialog_alert_material" + android:visibility="gone" + android:contentDescription="@string/notification_phishing_alert_content_description" + /> + + <ImageView + android:id="@+id/profile_badge" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:visibility="gone" + android:contentDescription="@string/notification_work_profile_content_description" + /> + + <ImageView + android:id="@+id/alerted_icon" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:contentDescription="@string/notification_alerted_content_description" + android:scaleType="fitCenter" + android:src="@drawable/ic_notifications_alerted" + android:visibility="gone" + /> +</merge> + diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index e6295ea06177..4ff3f8825cc4 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -101,6 +101,13 @@ P.S this is a change only intended for wear devices. --> <bool name="config_enableViewGroupScalingFading">true</bool> - <!-- Allow the gesture to double tap the power button to trigger a target action. --> - <bool name="config_doubleTapPowerGestureEnabled">false</bool> + <!-- Controls the double tap power button gesture to trigger a target action. + 0: Gesture is disabled + 1: Launch camera mode, allowing the user to disable/enable the double tap power gesture + from launching the camera application. + 2: Multi target mode, allowing the user to select one of the targets defined in + config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double + tap power gesture from triggering the selected target action. + --> + <integer name="config_doubleTapPowerGestureMode">0</integer> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 728c856f5855..8372aecf0d27 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7572,25 +7572,31 @@ <!-- NotificationProgressDrawable class --> <!-- ================================== --> - <!-- Drawable used to render a segmented bar, with segments and points. --> + <!-- Drawable used to render a notification progress bar, with segments and points. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawable"> - <!-- Default color for the parts. --> + <!-- The gap between two segments. --> <attr name="segSegGap" format="dimension" /> + <!-- The gap between a segment and a point. --> <attr name="segPointGap" format="dimension" /> </declare-styleable> <!-- Used to config the segments of a NotificationProgressDrawable. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawableSegments"> - <!-- Height of the solid segments --> + <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only + place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap + above. --> + <!-- Minimum required drawing width. The drawing width refers to the width after + the original segments have been adjusted for the neighboring Points and gaps. This is + enforced by stretching the segments that are too short. --> + <attr name="minWidth" format="dimension" /> + <!-- Height of the solid segments. --> <attr name="height" /> - <!-- Height of the faded segments --> - <attr name="fadedHeight" format="dimension"/> + <!-- Height of the faded segments. --> + <attr name="fadedHeight" format="dimension" /> <!-- Corner radius of the segment rect. --> <attr name="cornerRadius" format="dimension" /> - <!-- Default color of the segment. --> - <attr name="color" /> </declare-styleable> <!-- Used to config the points of a NotificationProgressDrawable. --> @@ -7602,8 +7608,6 @@ <attr name="inset" /> <!-- Corner radius of the point rect. --> <attr name="cornerRadius"/> - <!-- Default color of the point rect. --> - <attr name="color" /> </declare-styleable> <!-- ========================== --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index c50c5e9d3341..ce9a0c636d62 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2083,6 +2083,18 @@ See com.android.server.timezonedetector.TimeZoneDetectorStrategy for more information. --> <bool name="config_supportTelephonyTimeZoneFallback" translatable="false">true</bool> + <!-- Whether the time notifications feature is enabled. Settings this to false means the feature + cannot be used. Setting this to true means the feature can be enabled on the device. --> + <bool name="config_enableTimeZoneNotificationsSupported" translatable="false">true</bool> + + <!-- Whether the time zone notifications tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneNotificationsTrackingSupported" translatable="false">true</bool> + + <!-- Whether the time zone manual change tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneManualChangeTrackingSupported" translatable="false">true</bool> + <!-- Whether to enable network location overlay which allows network location provider to be replaced by an app at run-time. When disabled, only the config_networkLocationProviderPackageName package will be searched for network location diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 2adb79118ed9..d6b8704a978b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -608,12 +608,23 @@ <!-- Size of the feedback indicator for notifications --> <dimen name="notification_feedback_size">20dp</dimen> + <!-- Size of the (work) profile badge for notifications --> + <dimen name="notification_badge_size">12dp</dimen> + + <!-- Size of the (work) profile badge for notifications (2025 redesign version). + Scales with font size. Chosen to look good alongside notification_subtext_size text. --> + <dimen name="notification_2025_badge_size">14sp</dimen> + + <!-- Baseline for aligning icons in the top line (like the work profile icon or alerting icon) + to the text properly. This is equal to notification_2025_badge_size - 2sp. --> + <dimen name="notification_2025_badge_baseline">12sp</dimen> + + <!-- Spacing for the top line icons (e.g. the work profile badge). --> + <dimen name="notification_2025_badge_margin">4dp</dimen> + <!-- Size of the phishing alert for notifications --> <dimen name="notification_phishing_alert_size">@dimen/notification_badge_size</dimen> - <!-- Size of the profile badge for notifications --> - <dimen name="notification_badge_size">12dp</dimen> - <!-- Size of the alerted icon for notifications --> <dimen name="notification_alerted_size">@dimen/notification_badge_size</dimen> @@ -888,6 +899,8 @@ <dimen name="notification_progress_segSeg_gap">4dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <!-- The minimum required drawing width of the notification progress bar segments --> + <dimen name="notification_progress_segments_min_width">16dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The height of the notification progress bar faded segments --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 6313054e47f5..debc5e9a0dce 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -853,6 +853,9 @@ <!-- Text shown when viewing channel settings for notifications related to vpn status --> <string name="notification_channel_vpn">VPN status</string> + <!-- Text shown when viewing channel settings for notifications related to system time --> + <string name="notification_channel_system_time">Time and time zones</string> + <!-- Notification channel name. This channel sends high-priority alerts from the user's IT admin for key updates about the user's work device or work profile. --> <string name="notification_channel_device_admin">Alerts from your IT admin</string> @@ -881,6 +884,10 @@ <string name="notification_channel_accessibility_magnification">Magnification</string> <!-- Text shown when viewing channel settings for notifications related to accessibility + hearing device. [CHAR_LIMIT=NONE]--> + <string name="notification_channel_accessibility_hearing_device">Hearing device</string> + + <!-- Text shown when viewing channel settings for notifications related to accessibility security policy. [CHAR_LIMIT=NONE]--> <string name="notification_channel_accessibility_security_policy">Accessibility usage</string> @@ -3875,6 +3882,12 @@ <string name="carrier_app_notification_title">New SIM inserted</string> <string name="carrier_app_notification_text">Tap to set it up</string> + <!-- Time zone notification strings --> + <!-- Title for time zone change notifications --> + <string name="time_zone_change_notification_title">Your time zone changed</string> + <!-- Body for time zone change notifications --> + <string name="time_zone_change_notification_body">You\'re now in <xliff:g id="time_zone_display_name">%1$s</xliff:g> (<xliff:g id="time_zone_offset">%2$s</xliff:g>)</string> + <!-- Date/Time picker dialogs strings --> <!-- The title of the time picker dialog. [CHAR LIMIT=NONE] --> @@ -4985,6 +4998,19 @@ <!-- Text used to describe system navigation features, shown within a UI allowing a user to assign system magnification features to the Accessibility button in the navigation bar. --> <string name="accessibility_magnification_chooser_text">Magnification</string> + <!-- Notification title for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_phone_mic_notification_title">Switch to phone mic?</string> + <!-- Notification title for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_hearing_mic_notification_title">Switch to hearing aid mic?</string> + <!-- Notification content for switching input to the phone's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_phone_mic_notification_text">For better sound or if your hearing aid battery is low. This only switches your mic during the call.</string> + <!-- Notification content for switching input to the hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_switch_hearing_mic_notification_text">You can use your hearing aid microphone for hands-free calling. This only switches your mic during the call.</string> + <!-- Notification action button. Click it will switch the input between phone's microphone and hearing device's microphone. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_notification_switch_button">Switch</string> + <!-- Notification action button. Click it will open the bluetooth device details page for this hearing device. It will be shown when making a phone call with the hearing device. [CHAR LIMIT=none] --> + <string name="hearing_device_notification_settings_button">Settings</string> + <!-- Text spoken when the current user is switched if accessibility is enabled. [CHAR LIMIT=none] --> <string name="user_switched">Current user <xliff:g id="name" example="Bob">%1$s</xliff:g>.</string> <!-- Message shown when switching to a user [CHAR LIMIT=none] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 24e7057320ff..6c014e93d4cc 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,6 +575,7 @@ <java-symbol type="dimen" name="notification_top_pad_large_text" /> <java-symbol type="dimen" name="notification_top_pad_large_text_narrow" /> <java-symbol type="dimen" name="notification_badge_size" /> + <java-symbol type="dimen" name="notification_2025_badge_size" /> <java-symbol type="dimen" name="immersive_mode_cling_width" /> <java-symbol type="dimen" name="accessibility_magnification_indicator_width" /> <java-symbol type="dimen" name="circular_display_mask_thickness" /> @@ -2377,6 +2378,9 @@ <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" /> <java-symbol type="bool" name="config_enableTelephonyTimeZoneDetection" /> <java-symbol type="bool" name="config_supportTelephonyTimeZoneFallback" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsTrackingSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneManualChangeTrackingSupported" /> <java-symbol type="bool" name="config_autoResetAirplaneMode" /> <java-symbol type="string" name="config_notificationAccessConfirmationActivity" /> <java-symbol type="bool" name="config_preventImeStartupUnlessTextEditor" /> @@ -3839,6 +3843,13 @@ <java-symbol type="string" name="reduce_bright_colors_feature_name" /> <java-symbol type="string" name="one_handed_mode_feature_name" /> + <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_title" /> + <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_title" /> + <java-symbol type="string" name="hearing_device_switch_phone_mic_notification_text" /> + <java-symbol type="string" name="hearing_device_switch_hearing_mic_notification_text" /> + <java-symbol type="string" name="hearing_device_notification_switch_button" /> + <java-symbol type="string" name="hearing_device_notification_settings_button" /> + <!-- com.android.internal.widget.RecyclerView --> <java-symbol type="id" name="item_touch_helper_previous_elevation"/> <java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/> @@ -3939,6 +3950,7 @@ <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_progress_segSeg_gap" /> <java-symbol type="dimen" name="notification_progress_segPoint_gap" /> + <java-symbol type="dimen" name="notification_progress_segments_min_width" /> <java-symbol type="dimen" name="notification_progress_segments_height" /> <java-symbol type="dimen" name="notification_progress_segments_faded_height" /> <java-symbol type="dimen" name="notification_progress_segments_corner_radius" /> @@ -4018,6 +4030,7 @@ <java-symbol type="string" name="notification_channel_network_available" /> <java-symbol type="array" name="config_defaultCloudSearchServices" /> <java-symbol type="string" name="notification_channel_vpn" /> + <java-symbol type="string" name="notification_channel_system_time" /> <java-symbol type="string" name="notification_channel_device_admin" /> <java-symbol type="string" name="notification_channel_alerts" /> <java-symbol type="string" name="notification_channel_retail_mode" /> @@ -4025,8 +4038,11 @@ <java-symbol type="string" name="notification_channel_heavy_weight_app" /> <java-symbol type="string" name="notification_channel_system_changes" /> <java-symbol type="string" name="notification_channel_accessibility_magnification" /> + <java-symbol type="string" name="notification_channel_accessibility_hearing_device" /> <java-symbol type="string" name="notification_channel_accessibility_security_policy" /> <java-symbol type="string" name="notification_channel_display" /> + <java-symbol type="string" name="time_zone_change_notification_title" /> + <java-symbol type="string" name="time_zone_change_notification_body" /> <java-symbol type="string" name="config_defaultAutofillService" /> <java-symbol type="string" name="config_defaultFieldClassificationService" /> <java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" /> diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 6538ce85457c..3d6e1225bd92 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -16,6 +16,8 @@ package android.app; +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; @@ -25,8 +27,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ParceledListSlice; +import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -35,6 +41,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +49,7 @@ import org.junit.runner.RunWith; import java.time.Instant; import java.time.InstantSource; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest @@ -50,14 +58,24 @@ public class NotificationManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private Context mContext; private NotificationManagerWithMockService mNotificationManager; private final FakeClock mClock = new FakeClock(); + private PackageTestableContext mContext; + @Before public void setUp() { - mContext = ApplicationProvider.getApplicationContext(); + mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext()); mNotificationManager = new NotificationManagerWithMockService(mContext, mClock); + + // Caches must be in test mode in order to be used in tests. + PropertyInvalidatedCache.setTestMode(true); + mNotificationManager.setChannelCacheToTestMode(); + } + + @After + public void tearDown() { + PropertyInvalidatedCache.setTestMode(false); } @Test @@ -243,12 +261,161 @@ public class NotificationManagerTest { anyInt(), any(), anyInt()); } + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_cachedUntilInvalidated() throws Exception { + // Invalidate the cache first because the cache won't do anything until then + NotificationManager.invalidateNotificationChannelCache(); + + // It doesn't matter what the returned contents are, as long as we return a channel. + // This setup must set up getNotificationChannels(), as that's the method called. + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for the same channel 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_sameApp_oneCall() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + NotificationChannel c1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel c2 = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_NONE); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); + + assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2); + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannels_cachedUntilInvalidated() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for channels 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + List<NotificationChannel> unused = mNotificationManager.getNotificationChannels(); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + List<NotificationChannel> res = mNotificationManager.getNotificationChannels(); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + assertThat(res).containsExactlyElementsIn(List.of(exampleChannel())); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_channelAndConversationLookup() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + // Full list of channels: c1; conv1 = child of c1; c2 is unrelated + NotificationChannel c1 = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel conv1 = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + conv1.setConversationId("id", "id_conversation"); + NotificationChannel c2 = new NotificationChannel("other", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2))); + + // Lookup for channel c1 and c2: returned as expected + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2); + + // Lookup for conv1 should return conv1 + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + conv1); + + // Lookup for a different conversation channel that doesn't exist, whose parent channel id + // is "id", should return c1 + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1); + + // Lookup of a nonexistent channel is null + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + // All of that should have been one call to getNotificationChannels() + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_differentPackages() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + final String pkg1 = "one"; + final String pkg2 = "two"; + final int userId = 0; + final int userId1 = 1; + + // multiple channels with the same ID, but belonging to different packages/users + NotificationChannel channel1 = new NotificationChannel("id", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = channel1.copy(); + channel2.setName("name2"); + NotificationChannel channel3 = channel1.copy(); + channel3.setName("name3"); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3))); + + // set our context to pretend to be from package 1 and userId 0 + mContext.setParameters(pkg1, pkg1, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1); + + // now package 2 + mContext.setParameters(pkg2, pkg2, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2); + + // now pkg1 for a different user + mContext.setParameters(pkg1, pkg1, userId1); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3); + + // Those should have been three different calls + verify(mNotificationManager.mBackendService, times(3)) + .getNotificationChannels(any(), any(), anyInt()); + } + private Notification exampleNotification() { return new Notification.Builder(mContext, "channel") .setSmallIcon(android.R.drawable.star_big_on) .build(); } + private NotificationChannel exampleChannel() { + return new NotificationChannel("id", "channel_name", + NotificationManager.IMPORTANCE_DEFAULT); + } + private static class NotificationManagerWithMockService extends NotificationManager { private final INotificationManager mBackendService; @@ -264,6 +431,48 @@ public class NotificationManagerTest { } } + // Helper context wrapper class where we can control just the return values of getPackageName, + // getOpPackageName, and getUserId (used in getNotificationChannels). + private static class PackageTestableContext extends ContextWrapper { + private String mPackage; + private String mOpPackage; + private Integer mUserId; + + PackageTestableContext(Context base) { + super(base); + } + + void setParameters(String packageName, String opPackageName, int userId) { + mPackage = packageName; + mOpPackage = opPackageName; + mUserId = userId; + } + + @Override + public String getPackageName() { + if (mPackage != null) return mPackage; + return super.getPackageName(); + } + + @Override + public String getOpPackageName() { + if (mOpPackage != null) return mOpPackage; + return super.getOpPackageName(); + } + + @Override + public int getUserId() { + if (mUserId != null) return mUserId; + return super.getUserId(); + } + + @Override + public UserHandle getUser() { + if (mUserId != null) return UserHandle.of(mUserId); + return super.getUser(); + } + } + private static class FakeClock implements InstantSource { private long mNowMillis = 441644400000L; diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..7be6950fb613 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2504,6 +2504,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressSegments() { + final List<Notification.ProgressStyle.Segment> segments = List.of( + new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE), + new Notification.ProgressStyle.Segment(50).setColor(Color.RED), + new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressSegments(segments); + + assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_addProgressPoint_dropsNegativePoints() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2532,6 +2547,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoints() { + final List<Notification.ProgressStyle.Point> points = List.of( + new Notification.ProgressStyle.Point(0).setColor(Color.WHITE), + new Notification.ProgressStyle.Point(50).setColor(Color.RED), + new Notification.ProgressStyle.Point(100).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressPoints(points); + + assertThat(progressStyle1.getProgressPoints()).isEqualTo(points); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2673,11 +2703,58 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressIndeterminate() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressIndeterminate(true); + assertThat(progressStyle1.isProgressIndeterminate()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_styledByProgress_defaultValueTrue() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); assertThat(progressStyle1.isStyledByProgress()).isTrue(); } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setStyledByProgress() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setStyledByProgress(false); + assertThat(progressStyle1.isStyledByProgress()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_point() { + final int id = 1; + final int position = 10; + final int color = Color.RED; + + final Notification.ProgressStyle.Point point = + new Notification.ProgressStyle.Point(position).setId(id).setColor(color); + + assertEquals(id, point.getId()); + assertEquals(position, point.getPosition()); + assertEquals(color, point.getColor()); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_segment() { + final int id = 1; + final int length = 100; + final int color = Color.RED; + + final Notification.ProgressStyle.Segment segment = + new Notification.ProgressStyle.Segment(length).setId(id).setColor(color); + + assertEquals(id, segment.getId()); + assertEquals(length, segment.getLength()); + assertEquals(color, segment.getColor()); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java index 0bf406c970f2..2bd3f4df9435 100644 --- a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java +++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java @@ -17,6 +17,7 @@ package com.android.internal.notification; import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS; +import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE; import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION; import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY; import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT; @@ -90,8 +91,8 @@ public class SystemNotificationChannelsTest { DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS, NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB, FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES, - ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY, - ABUSIVE_BACKGROUND_APPS); + ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_HEARING_DEVICE, + ACCESSIBILITY_SECURITY_POLICY, ABUSIVE_BACKGROUND_APPS); } @Test diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index d26bb35e5481..f105ec305eab 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -20,12 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Notification.ProgressStyle; import android.graphics.Color; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressBar.Part; +import com.android.internal.widget.NotificationProgressBar.Point; +import com.android.internal.widget.NotificationProgressBar.Segment; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,183 +38,303 @@ import java.util.List; public class NotificationProgressBarTest { @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsIsEmpty() { + public void processAndConvertToParts_segmentsIsEmpty() { List<ProgressStyle.Segment> segments = new ArrayList<>(); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() { + public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsNegative() { + public void processAndConvertToParts_segmentLengthIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(-50)); segments.add(new ProgressStyle.Segment(150)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsZero() { + public void processAndConvertToParts_segmentLengthIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(0)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressIsNegative() { + public void processAndConvertToParts_progressIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = -50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_progressIsZero() { + public void processAndConvertToParts_progressIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 0; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, fadedRed, true))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true))); - - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(0); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_progressAtMax() { + public void processAndConvertToParts_progressAtMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 100; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); - assertThat(parts).isEqualTo(expected); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(300); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressAboveMax() { + public void processAndConvertToParts_progressAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 150; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionIsNegative() { + public void processAndConvertToParts_pointPositionIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(-50).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionAboveMax() { + public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(150).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() { + public void processAndConvertToParts_multipleSegmentsWithoutPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Segment(0.50f, Color.RED), + new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 180, Color.GREEN), + new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + int progress = 60; + int progressMax = 100; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.50f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Segment(0.40f, fadedGreen, true))); + new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = false; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 176, Color.GREEN), + new NotificationProgressDrawable.Segment(180, 300, fadedGreen, true))); - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_singleSegmentWithPoints() { + public void processAndConvertToParts_singleSegmentWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); @@ -223,31 +344,77 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - // Colors with 40% opacity - int fadedBlue = 0x660000FF; - int fadedYellow = 0x66FFFF00; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.BLUE), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.BLUE), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.35f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedBlue, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedBlue, true))); + new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), + new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.BLUE), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.BLUE), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 170, Color.BLUE), + new NotificationProgressDrawable.Point(174, 186, Color.BLUE), + new NotificationProgressDrawable.Segment(190, 215, Color.BLUE), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedBlue = 0x660000FF; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.219177F, Color.BLUE), + new NotificationProgressDrawable.Point(38.219177F, 50.219177F, Color.RED), + new NotificationProgressDrawable.Segment(54.219177F, 70.21918F, Color.BLUE), + new NotificationProgressDrawable.Point(74.21918F, 86.21918F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.21918F, 172.38356F, Color.BLUE), + new NotificationProgressDrawable.Point(176.38356F, 188.38356F, Color.BLUE), + new NotificationProgressDrawable.Segment(192.38356F, 217.0137F, fadedBlue, + true), + new NotificationProgressDrawable.Point(221.0137F, 233.0137F, fadedYellow), + new NotificationProgressDrawable.Segment(237.0137F, 300F, fadedBlue, + true))); + + assertThat(p.second).isEqualTo(182.38356F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() { + public void processAndConvertToParts_multipleSegmentsWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -258,32 +425,81 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); - // Colors with 40% opacity - int fadedGreen = 0x6600FF00; - int fadedYellow = 0x66FFFF00; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.RED), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.25f, Color.RED), new Segment(0.10f, Color.GREEN), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedGreen, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedGreen, true))); + new Point(Color.BLUE), + new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.RED), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 170, Color.GREEN), + new NotificationProgressDrawable.Point(174, 186, Color.BLUE), + new NotificationProgressDrawable.Segment(190, 215, Color.GREEN), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.095238F, Color.RED), + new NotificationProgressDrawable.Point(38.095238F, 50.095238F, Color.RED), + new NotificationProgressDrawable.Segment(54.095238F, 70.09524F, Color.RED), + new NotificationProgressDrawable.Point(74.09524F, 86.09524F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.09524F, 148.9524F, Color.RED), + new NotificationProgressDrawable.Segment(152.95238F, 172.7619F, + Color.GREEN), + new NotificationProgressDrawable.Point(176.7619F, 188.7619F, Color.BLUE), + new NotificationProgressDrawable.Segment(192.7619F, 217.33333F, + fadedGreen, true), + new NotificationProgressDrawable.Point(221.33333F, 233.33333F, + fadedYellow), + new NotificationProgressDrawable.Segment(237.33333F, 299.99997F, + fadedGreen, true))); + + assertThat(p.second).isEqualTo(182.7619F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() { + public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -293,21 +509,251 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = false; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); - List<Part> expected = new ArrayList<>(List.of( + List<Part> expectedParts = new ArrayList<>(List.of( new Segment(0.15f, Color.RED), - new Point(null, Color.RED), + new Point(Color.RED), new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), + new Point(Color.BLUE), new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), - new Point(null, Color.YELLOW), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); - assertThat(parts).isEqualTo(expected); + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 35, Color.RED), + new NotificationProgressDrawable.Point(39, 51, Color.RED), + new NotificationProgressDrawable.Segment(55, 65, Color.RED), + new NotificationProgressDrawable.Point(69, 81, Color.BLUE), + new NotificationProgressDrawable.Segment(85, 146, Color.RED), + new NotificationProgressDrawable.Segment(150, 215, Color.GREEN), + new NotificationProgressDrawable.Point(219, 231, Color.YELLOW), + new NotificationProgressDrawable.Segment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = false; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 300, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Segment(0, 34.296295F, Color.RED), + new NotificationProgressDrawable.Point(38.296295F, 50.296295F, Color.RED), + new NotificationProgressDrawable.Segment(54.296295F, 70.296295F, Color.RED), + new NotificationProgressDrawable.Point(74.296295F, 86.296295F, Color.BLUE), + new NotificationProgressDrawable.Segment(90.296295F, 149.62962F, Color.RED), + new NotificationProgressDrawable.Segment(153.62962F, 216.8148F, + Color.GREEN), + new NotificationProgressDrawable.Point(220.81482F, 232.81482F, + Color.YELLOW), + new NotificationProgressDrawable.Segment(236.81482F, 300, Color.GREEN))); + + assertThat(p.second).isEqualTo(182.9037F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // segmentMinWidth (= 16dp). + @Test + public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), + new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 32, Color.BLUE), + new NotificationProgressDrawable.Segment(36, 69.41936F, Color.BLUE), + new NotificationProgressDrawable.Segment(73.41936F, 124.25807F, Color.BLUE), + new NotificationProgressDrawable.Segment(128.25807F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // segmentMinWidth (= 10dp). + @Test + public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 16, Color.BLUE), + new NotificationProgressDrawable.Segment(20, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 26, Color.BLUE), + new NotificationProgressDrawable.Segment(30, 64.169014F, Color.BLUE), + new NotificationProgressDrawable.Segment(68.169014F, 120.92958F, + Color.BLUE), + new NotificationProgressDrawable.Segment(124.92958F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void maybeStretchAndRescaleSegments_noStretchingNecessary() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts( + segments, points, progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of( + new Point(Color.BLUE), + new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), + new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<NotificationProgressDrawable.Part> drawableParts = + NotificationProgressBar.processAndConvertToDrawableParts(parts, drawableWidth, + segSegGap, segPointGap, pointRadius, hasTrackerIcon + ); + + List<NotificationProgressDrawable.Part> expectedDrawableParts = new ArrayList<>( + List.of(new NotificationProgressDrawable.Point(0, 12, Color.BLUE), + new NotificationProgressDrawable.Segment(16, 36, Color.BLUE), + new NotificationProgressDrawable.Segment(40, 56, Color.BLUE), + new NotificationProgressDrawable.Segment(60, 116, Color.BLUE), + new NotificationProgressDrawable.Segment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<NotificationProgressDrawable.Part>, Float> p = + NotificationProgressBar.maybeStretchAndRescaleSegments(parts, drawableParts, + segmentMinWidth, pointRadius, (float) progress / progressMax, 200, + isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java index e368d2815855..cb8b5ce245b6 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java @@ -48,12 +48,14 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); { TimeZoneCapabilities one = builder1.build(); TimeZoneCapabilities two = builder2.build(); @@ -115,6 +117,13 @@ public class TimeZoneCapabilitiesTest { TimeZoneCapabilities two = builder2.build(); assertEquals(one, two); } + + builder1.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertNotEquals(one, two); + } } @Test @@ -123,7 +132,8 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); assertRoundTripParcelable(builder.build()); builder.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED); @@ -137,6 +147,9 @@ public class TimeZoneCapabilitiesTest { builder.setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); + + builder.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + assertRoundTripParcelable(builder.build()); } @Test @@ -151,6 +164,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -175,6 +189,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -191,6 +206,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); { @@ -204,6 +220,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -221,6 +238,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(false) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -238,6 +256,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -255,6 +274,25 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) + .build(); + + assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); + } + + { + TimeZoneCapabilities updatedCapabilities = + new TimeZoneCapabilities.Builder(capabilities) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) + .build(); + + TimeZoneCapabilities expectedCapabilities = + new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) + .setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setUseLocationEnabled(true) + .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); diff --git a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java index 4ad3e41383aa..345e91268253 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java @@ -43,9 +43,11 @@ public class TimeZoneConfigurationTest { TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(true) .setGeoDetectionEnabled(true) + .setNotificationsEnabled(true) .build(); assertTrue(completeConfig.isComplete()); assertTrue(completeConfig.hasIsGeoDetectionEnabled()); + assertTrue(completeConfig.hasIsNotificationsEnabled()); } @Test diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 4c75ea4777da..957d1b835ec2 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -26,8 +26,8 @@ package { java_library { name: "wm_shell_protolog-groups", srcs: [ - "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", ":protolog-common-src", + "src/com/android/wm/shell/protolog/ShellProtoLogGroup.java", ], } @@ -61,8 +61,8 @@ java_genrule { name: "wm_shell_protolog_src", srcs: [ ":protolog-impl", - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) transform-protolog-calls " + @@ -80,8 +80,8 @@ java_genrule { java_genrule { name: "generate-wm_shell_protolog.json", srcs: [ - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + @@ -97,8 +97,8 @@ java_genrule { java_genrule { name: "gen-wmshell.protolog.pb", srcs: [ - ":wm_shell_protolog-groups", ":wm_shell-sources", + ":wm_shell_protolog-groups", ], tools: ["protologtool"], cmd: "$(location protologtool) generate-viewer-config " + @@ -159,38 +159,39 @@ java_library { android_library { name: "WindowManager-Shell", srcs: [ - "src/com/android/wm/shell/EventLogTags.logtags", ":wm_shell_protolog_src", // TODO(b/168581922) protologtool do not support kotlin(*.kt) - ":wm_shell-sources-kt", + "src/com/android/wm/shell/EventLogTags.logtags", ":wm_shell-aidls", ":wm_shell-shared-aidls", + ":wm_shell-sources-kt", ], resource_dirs: [ "res", ], static_libs: [ + "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", + "//frameworks/libs/systemui:iconloader_base", + "//packages/apps/Car/SystemUI/aconfig:com_android_systemui_car_flags_lib", + "PlatformAnimationLib", + "WindowManager-Shell-lite-proto", + "WindowManager-Shell-proto", + "WindowManager-Shell-shared", + "androidx-constraintlayout_constraintlayout", "androidx.appcompat_appcompat", - "androidx.core_core-ktx", "androidx.arch.core_core-runtime", - "androidx.datastore_datastore", "androidx.compose.material3_material3", - "androidx-constraintlayout_constraintlayout", + "androidx.core_core-ktx", + "androidx.datastore_datastore", "androidx.dynamicanimation_dynamicanimation", "androidx.recyclerview_recyclerview", - "kotlinx-coroutines-android", - "kotlinx-coroutines-core", - "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", - "//frameworks/libs/systemui:iconloader_base", "com_android_launcher3_flags_lib", "com_android_wm_shell_flags_lib", - "PlatformAnimationLib", - "WindowManager-Shell-proto", - "WindowManager-Shell-lite-proto", - "WindowManager-Shell-shared", - "perfetto_trace_java_protos", "dagger2", "jsr330", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", + "perfetto_trace_java_protos", ], libs: [ // Soong fails to automatically add this dependency because all the diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 755f472ee22e..2fed1380b635 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -233,6 +233,16 @@ public class DesktopModeStatus { } /** + * Returns whether the multiple desktops feature is enabled for this device (both backend and + * frontend implementations). + */ + public static boolean enableMultipleDesktops(@NonNull Context context) { + return Flags.enableMultipleDesktopsBackend() + && Flags.enableMultipleDesktopsFrontend() + && canEnterDesktopMode(context); + } + + /** * @return {@code true} if this device is requesting to show the app handle despite non * necessarily enabling desktop mode */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 9f01316d5b5c..b098620fde2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -230,6 +230,11 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { return mDisplayAreasInfo.get(displayId); } + @Nullable + public SurfaceControl getDisplayAreaLeash(int displayId) { + return mLeashes.get(displayId); + } + /** * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by * {@link DisplayAreaInfo#displayId}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java new file mode 100644 index 000000000000..fc51c754e06b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoShellModule.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive; + +import com.android.wm.shell.dagger.WMSingleton; + +import dagger.Binds; +import dagger.Module; + + +@Module +public abstract class AutoShellModule { + @WMSingleton + @Binds + abstract AutoTaskStackController provideTaskStackController(AutoTaskStackControllerImpl impl); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt new file mode 100644 index 000000000000..caacdd355996 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStack.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive + +import android.app.ActivityManager +import android.graphics.Rect +import android.view.SurfaceControl + +/** + * Represents an auto task stack, which is always in multi-window mode. + * + * @property id The ID of the task stack. + * @property displayId The ID of the display the task stack is on. + * @property leash The surface control leash of the task stack. + */ +interface AutoTaskStack { + val id: Int + val displayId: Int + var leash: SurfaceControl +} + +/** + * Data class representing the state of an auto task stack. + * + * @property bounds The bounds of the task stack. + * @property childrenTasksVisible Whether the child tasks of the stack are visible. + * @property layer The layer of the task stack. + */ +data class AutoTaskStackState( + val bounds: Rect = Rect(), + val childrenTasksVisible: Boolean, + val layer: Int +) + +/** + * Data class representing a root task stack. + * + * @property id The ID of the root task stack + * @property displayId The ID of the display the root task stack is on. + * @property leash The surface control leash of the root task stack. + * @property rootTaskInfo The running task info of the root task. + */ +data class RootTaskStack( + override val id: Int, + override val displayId: Int, + override var leash: SurfaceControl, + var rootTaskInfo: ActivityManager.RunningTaskInfo +) : AutoTaskStack diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt new file mode 100644 index 000000000000..15fedac62af3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackController.kt @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive + +import android.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import android.os.IBinder +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback + +/** + * Delegate interface for handling auto task stack transitions. + */ +interface AutoTaskStackTransitionHandlerDelegate { + /** + * Handles a transition request. + * + * @param transition The transition identifier. + * @param request The transition request information. + * @return An [AutoTaskStackTransaction] to be applied for the transition, or null if the + * animation is not handled by this delegate. + */ + fun handleRequest( + transition: IBinder, request: TransitionRequestInfo + ): AutoTaskStackTransaction? + + /** + * See [Transitions.TransitionHandler.startAnimation] for more details. + * + * @param changedTaskStacks Contains the states of the task stacks that were changed as a + * result of this transition. The key is the [AutoTaskStack.id] and the value is the + * corresponding [AutoTaskStackState]. + */ + fun startAnimation( + transition: IBinder, + changedTaskStacks: Map<Int, AutoTaskStackState>, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback + ): Boolean + + /** + * See [Transitions.TransitionHandler.onTransitionConsumed] for more details. + * + * @param requestedTaskStacks contains the states of the task stacks that were requested in + * the transition. The key is the [AutoTaskStack.id] and the value is the corresponding + * [AutoTaskStackState]. + */ + fun onTransitionConsumed( + transition: IBinder, + requestedTaskStacks: Map<Int, AutoTaskStackState>, + aborted: Boolean, finishTransaction: SurfaceControl.Transaction? + ) + + /** + * See [Transitions.TransitionHandler.mergeAnimation] for more details. + * + * @param changedTaskStacks Contains the states of the task stacks that were changed as a + * result of this transition. The key is the [AutoTaskStack.id] and the value is the + * corresponding [AutoTaskStackState]. + */ + fun mergeAnimation( + transition: IBinder, + changedTaskStacks: Map<Int, AutoTaskStackState>, + info: TransitionInfo, + surfaceTransaction: SurfaceControl.Transaction, + mergeTarget: IBinder, + finishCallback: TransitionFinishCallback + ) +} + + +/** + * Controller for managing auto task stacks. + */ +interface AutoTaskStackController { + + var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate? + set + + /** + * Map of task stack IDs to their states. + * + * This gets updated right before [AutoTaskStackTransitionHandlerDelegate.startAnimation] or + * [AutoTaskStackTransitionHandlerDelegate.onTransitionConsumed] is called. + */ + val taskStackStateMap: Map<Int, AutoTaskStackState> + get + + /** + * Creates a new multi-window root task. + * + * A root task stack is placed in the default TDA of the specified display by default. + * Once the root task is removed, the [AutoTaskStackController] no longer holds a reference to + * it. + * + * @param displayId The ID of the display to create the root task stack on. + * @param listener The listener for root task stack events. + */ + @ShellMainThread + fun createRootTaskStack(displayId: Int, listener: RootTaskStackListener) + + + /** + * Sets the default root task stack (launch root) on a display. Calling it again with a + * different [rootTaskStackId] will simply replace the default root task stack on the display. + * + * Note: This is helpful for passively routing tasks to a specified container. If a display + * doesn't have a default root task stack set, all tasks will open in fullscreen and cover + * the entire default TDA by default. + * + * @param displayId The ID of the display. + * @param rootTaskStackId The ID of the root task stack, or null to clear the default. + */ + @ShellMainThread + fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) + + /** + * Starts a transaction with the specified [transaction]. + * Returns the transition identifier. + */ + @ShellMainThread + fun startTransition(transaction: AutoTaskStackTransaction): IBinder? +} + +internal sealed class TaskStackOperation { + data class ReparentTask( + val taskId: Int, + val parentTaskStackId: Int, + val onTop: Boolean + ) : TaskStackOperation() + + data class SendPendingIntent( + val sender: PendingIntent, + val intent: Intent, + val options: Bundle? + ) : TaskStackOperation() + + data class SetTaskStackState( + val taskStackId: Int, + val state: AutoTaskStackState + ) : TaskStackOperation() +} + +data class AutoTaskStackTransaction internal constructor( + internal val operations: MutableList<TaskStackOperation> = mutableListOf() +) { + constructor() : this( + mutableListOf() + ) + + /** See [WindowContainerTransaction.reparent] for more details. */ + fun reparentTask( + taskId: Int, + parentTaskStackId: Int, + onTop: Boolean + ): AutoTaskStackTransaction { + operations.add(TaskStackOperation.ReparentTask(taskId, parentTaskStackId, onTop)) + return this + } + + /** See [WindowContainerTransaction.sendPendingIntent] for more details. */ + fun sendPendingIntent( + sender: PendingIntent, + intent: Intent, + options: Bundle? + ): AutoTaskStackTransaction { + operations.add(TaskStackOperation.SendPendingIntent(sender, intent, options)) + return this + } + + /** + * Adds a set task stack state operation to the transaction. + * + * If an operation with the same task stack ID already exists, it is replaced with the new one. + * + * @param taskStackId The ID of the task stack. + * @param state The new state of the task stack. + * @return The transaction with the added operation. + */ + fun setTaskStackState(taskStackId: Int, state: AutoTaskStackState): AutoTaskStackTransaction { + val existingOperation = operations.find { + it is TaskStackOperation.SetTaskStackState && it.taskStackId == taskStackId + } + if (existingOperation != null) { + val index = operations.indexOf(existingOperation) + operations[index] = TaskStackOperation.SetTaskStackState(taskStackId, state) + } else { + operations.add(TaskStackOperation.SetTaskStackState(taskStackId, state)) + } + return this + } + + /** + * Returns a map of task stack IDs to their states from the set task stack state operations. + * + * @return The map of task stack IDs to states. + */ + fun getTaskStackStates(): Map<Int, AutoTaskStackState> { + val states = mutableMapOf<Int, AutoTaskStackState>() + operations.forEach { operation -> + if (operation is TaskStackOperation.SetTaskStackState) { + states[operation.taskStackId] = operation.state + } + } + return states + } +} + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt new file mode 100644 index 000000000000..f8f284238a98 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt @@ -0,0 +1,534 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive + +import android.app.ActivityManager +import android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT +import android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.os.IBinder +import android.util.Log +import android.util.Slog +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.systemui.car.Flags.autoTaskStackWindowing +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback +import javax.inject.Inject + +const val TAG = "AutoTaskStackController" + +@WMSingleton +class AutoTaskStackControllerImpl @Inject constructor( + val taskOrganizer: ShellTaskOrganizer, + @ShellMainThread private val shellMainThread: ShellExecutor, + val transitions: Transitions, + val shellInit: ShellInit, + val rootTdaOrganizer: RootTaskDisplayAreaOrganizer +) : AutoTaskStackController, Transitions.TransitionHandler { + override var autoTransitionHandlerDelegate: AutoTaskStackTransitionHandlerDelegate? = null + override val taskStackStateMap = mutableMapOf<Int, AutoTaskStackState>() + + private val DBG = Log.isLoggable(TAG, Log.DEBUG) + private val taskStackMap = mutableMapOf<Int, AutoTaskStack>() + private val pendingTransitions = ArrayList<PendingTransition>() + private val mTaskStackStateTranslator = TaskStackStateTranslator() + private val appTasksMap = mutableMapOf<Int, ActivityManager.RunningTaskInfo>() + private val defaultRootTaskPerDisplay = mutableMapOf<Int, Int>() + + init { + if (!autoTaskStackWindowing()) { + throw IllegalStateException("Failed to initialize" + + "AutoTaskStackController as the auto_task_stack_windowing TS flag is disabled.") + } else { + shellInit.addInitCallback(this::onInit, this); + } + } + + fun onInit() { + transitions.addHandler(this) + } + + /** Translates the [AutoTaskStackState] to relevant WM and surface transactions. */ + inner class TaskStackStateTranslator { + // TODO(b/384946072): Move to an interface with 2 implementations, one for root task and + // other for TDA + fun applyVisibilityAndBounds( + wct: WindowContainerTransaction, + taskStack: AutoTaskStack, + state: AutoTaskStackState + ) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to convertToWct") + return + } + wct.setBounds(taskStack.rootTaskInfo.token, state.bounds) + wct.reorder(taskStack.rootTaskInfo.token, /* onTop= */ state.childrenTasksVisible) + } + + fun reorderLeash( + taskStack: AutoTaskStack, + state: AutoTaskStackState, + transaction: Transaction + ) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to reorder leash") + return + } + Slog.d(TAG, "Setting the layer ${state.layer}") + transaction.setLayer(taskStack.leash, state.layer) + } + + fun restoreLeash(taskStack: AutoTaskStack, transaction: Transaction) { + if (taskStack !is RootTaskStack) { + Slog.e(TAG, "Unsupported task stack, unable to restore leash") + return + } + + val rootTdaInfo = rootTdaOrganizer.getDisplayAreaInfo(taskStack.displayId) + if (rootTdaInfo == null || + rootTdaInfo.featureId != taskStack.rootTaskInfo.displayAreaFeatureId + ) { + Slog.e(TAG, "Cannot find the rootTDA for the root task stack ${taskStack.id}") + return + } + if (DBG) { + Slog.d(TAG, "Reparenting ${taskStack.id} leash to DA ${rootTdaInfo.featureId}") + } + transaction.reparent( + taskStack.leash, + rootTdaOrganizer.getDisplayAreaLeash(taskStack.displayId) + ) + } + } + + inner class RootTaskStackListenerAdapter( + val rootTaskStackListener: RootTaskStackListener, + ) : ShellTaskOrganizer.TaskListener { + private var rootTaskStack: RootTaskStack? = null + + // TODO(b/384948029): Notify car service for all the children tasks' events + override fun onTaskAppeared( + taskInfo: ActivityManager.RunningTaskInfo?, + leash: SurfaceControl? + ) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskAppeared") + } + if (leash == null) { + throw IllegalArgumentException("leash can't be null in onTaskAppeared") + } + if (DBG) Slog.d(TAG, "onTaskAppeared = ${taskInfo.taskId}") + + if (rootTaskStack == null) { + val rootTask = + RootTaskStack(taskInfo.taskId, taskInfo.displayId, leash, taskInfo) + taskStackMap[rootTask.id] = rootTask + + rootTaskStack = rootTask; + rootTaskStackListener.onRootTaskStackCreated(rootTask); + return + } + appTasksMap[taskInfo.taskId] = taskInfo + rootTaskStackListener.onTaskAppeared(taskInfo, leash) + } + + override fun onTaskInfoChanged(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskInfoChanged") + } + if (DBG) Slog.d(TAG, "onTaskInfoChanged = ${taskInfo.taskId}") + var previousRootTaskStackInfo = rootTaskStack ?: run { + Slog.e(TAG, "Received onTaskInfoChanged, when root task stack is null") + return@onTaskInfoChanged + } + rootTaskStack?.let { + if (taskInfo.taskId == previousRootTaskStackInfo.id) { + previousRootTaskStackInfo = previousRootTaskStackInfo.copy(rootTaskInfo = taskInfo) + taskStackMap[previousRootTaskStackInfo.id] = previousRootTaskStackInfo + rootTaskStack = previousRootTaskStackInfo; + rootTaskStackListener.onRootTaskStackInfoChanged(it) + return + } + } + + appTasksMap[taskInfo.taskId] = taskInfo + rootTaskStackListener.onTaskInfoChanged(taskInfo) + } + + override fun onTaskVanished(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onTaskVanished") + } + if (DBG) Slog.d(TAG, "onTaskVanished = ${taskInfo.taskId}") + var rootTask = rootTaskStack ?: run { + Slog.e(TAG, "Received onTaskVanished, when root task stack is null") + return@onTaskVanished + } + if (taskInfo.taskId == rootTask.id) { + rootTask = rootTask.copy(rootTaskInfo = taskInfo) + rootTaskStack = rootTask + rootTaskStackListener.onRootTaskStackDestroyed(rootTask) + taskStackMap.remove(rootTask.id) + taskStackStateMap.remove(rootTask.id) + rootTaskStack = null + return + } + appTasksMap.remove(taskInfo.taskId) + rootTaskStackListener.onTaskVanished(taskInfo) + } + + override fun onBackPressedOnTaskRoot(taskInfo: ActivityManager.RunningTaskInfo?) { + if (taskInfo == null) { + throw IllegalArgumentException("taskInfo can't be null in onBackPressedOnTaskRoot") + } + super.onBackPressedOnTaskRoot(taskInfo) + rootTaskStackListener.onBackPressedOnTaskRoot(taskInfo) + } + } + + override fun createRootTaskStack( + displayId: Int, + listener: RootTaskStackListener + ) { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to create root task stack as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return + } + taskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_MULTI_WINDOW, + RootTaskStackListenerAdapter(listener), + /* removeWithTaskOrganizer= */ true + ) + } + + override fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to set default root task stack as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return + } + var wct = WindowContainerTransaction() + + // Clear the default root task stack if already set + defaultRootTaskPerDisplay[displayId]?.let { existingDefaultRootTaskStackId -> + (taskStackMap[existingDefaultRootTaskStackId] as? RootTaskStack)?.let { rootTaskStack -> + wct.setLaunchRoot(rootTaskStack.rootTaskInfo.token, null, null) + } + } + + if (rootTaskStackId != null) { + var taskStack = + taskStackMap[rootTaskStackId] ?: run { return@setDefaultRootTaskStackOnDisplay } + if (DBG) Slog.d(TAG, "setting launch root for = ${taskStack.id}") + if (taskStack !is RootTaskStack) { + throw IllegalArgumentException( + "Cannot set a non root task stack as default root task " + + "stack" + ) + } + wct.setLaunchRoot( + taskStack.rootTaskInfo.token, + intArrayOf(WINDOWING_MODE_UNDEFINED), + intArrayOf( + ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_RECENTS, + + // TODO(b/386242708): Figure out if this flag will ever be used for automotive + // assistant. Based on output, remove it from here and fix the + // AssistantStackTests accordingly. + ACTIVITY_TYPE_ASSISTANT + ) + ) + } + + taskOrganizer.applyTransaction(wct) + } + + override fun startTransition(transaction: AutoTaskStackTransaction): IBinder? { + if (!autoTaskStackWindowing()) { + Slog.e( + TAG, "Failed to start transaction as the " + + "auto_task_stack_windowing TS flag is disabled." + ) + return null + } + if (transaction.operations.isEmpty()) { + Slog.e(TAG, "Operations empty, no transaction started") + return null + } + if (DBG) Slog.d(TAG, "startTransaction ${transaction.operations}") + + var wct = WindowContainerTransaction() + convertToWct(transaction, wct) + var pending = PendingTransition( + TRANSIT_CHANGE, + wct, + transaction, + ) + return startTransitionNow(pending) + } + + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo + ): WindowContainerTransaction? { + if (DBG) { + Slog.d(TAG, "handle request, id=${request.debugId}, type=${request.type}, " + + "triggertask = ${request.triggerTask ?: "null"}") + } + val ast = autoTransitionHandlerDelegate?.handleRequest(transition, request) + ?: run { return@handleRequest null } + + if (ast.operations.isEmpty()) { + return null + } + var wct = WindowContainerTransaction() + convertToWct(ast, wct) + + pendingTransitions.add( + PendingTransition(request.type, wct, ast).apply { isClaimed = transition } + ) + return wct + } + + fun updateTaskStackStates(taskStatStates: Map<Int, AutoTaskStackState>) { + taskStackStateMap.putAll(taskStatStates) + } + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: TransitionFinishCallback + ): Boolean { + if (DBG) Slog.d(TAG, " startAnimation, id=${info.debugId} = changes=" + info.changes) + val pending: PendingTransition? = findPending(transition) + if (pending != null) { + pendingTransitions.remove(pending) + updateTaskStackStates(pending.transaction.getTaskStackStates()) + } + + reorderLeashes(startTransaction) + reorderLeashes(finishTransaction) + + for (chg in info.changes) { + // TODO(b/384946072): handle the da stack similarly. The below implementation only + // handles the root task stack + + val taskInfo = chg.taskInfo ?: continue + val taskStack = taskStackMap[taskInfo.taskId] ?: continue + + // Restore the leashes for the task stacks to ensure correct z-order competition + if (taskStackMap.containsKey(taskInfo.taskId)) { + mTaskStackStateTranslator.restoreLeash( + taskStack, + startTransaction + ) + if (TransitionUtil.isOpeningMode(chg.mode)) { + // Clients can still manipulate the alpha, but this ensures that the default + // behavior is natural + startTransaction.setAlpha(chg.leash, 1f) + } + continue + } + } + + val isPlayedByDelegate = autoTransitionHandlerDelegate?.startAnimation( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + info, + startTransaction, + finishTransaction, + { + shellMainThread.execute { + finishCallback.onTransitionFinished(it) + startNextTransition() + } + } + ) ?: false + + if (isPlayedByDelegate) { + if (DBG) Slog.d(TAG, "${info.debugId} played"); + return true; + } + + // If for an animation which is not played by the delegate, contains a change in a known + // task stack, it should be leveraged to correct the leashes. So, handle the animation in + // this case. + if (info.changes.any { taskStackMap.containsKey(it.taskInfo?.taskId) }) { + startTransaction.apply() + finishCallback.onTransitionFinished(null) + startNextTransition() + if (DBG) Slog.d(TAG, "${info.debugId} played"); + return true + } + return false; + } + + fun convertToWct(ast: AutoTaskStackTransaction, wct: WindowContainerTransaction) { + ast.operations.forEach { operation -> + when (operation) { + is TaskStackOperation.ReparentTask -> { + val appTask = appTasksMap[operation.taskId] + + if (appTask == null) { + Slog.e( + TAG, "task with id=$operation.taskId not found, failed to " + + "reparent." + ) + return@forEach + } + if (!taskStackMap.containsKey(operation.parentTaskStackId)) { + Slog.e( + TAG, "task stack with id=${operation.parentTaskStackId} not " + + "found, failed to reparent" + ) + return@forEach + } + // TODO(b/384946072): Handle a display area stack as well + wct.reparent( + appTask.token, + (taskStackMap[operation.parentTaskStackId] as RootTaskStack) + .rootTaskInfo.token, + operation.onTop + ) + } + + is TaskStackOperation.SendPendingIntent -> wct.sendPendingIntent( + operation.sender, + operation.intent, + operation.options + ) + + is TaskStackOperation.SetTaskStackState -> { + taskStackMap[operation.taskStackId]?.let { taskStack -> + mTaskStackStateTranslator.applyVisibilityAndBounds( + wct, + taskStack, + operation.state + ) + } + ?: Slog.w(TAG, "AutoTaskStack with id ${operation.taskStackId} " + + "not found.") + } + } + } + } + + override fun mergeAnimation( + transition: IBinder, + info: TransitionInfo, + surfaceTransaction: Transaction, + mergeTarget: IBinder, + finishCallback: TransitionFinishCallback + ) { + val pending: PendingTransition? = findPending(transition) + + autoTransitionHandlerDelegate?.mergeAnimation( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + info, + surfaceTransaction, + mergeTarget, + /* finishCallback = */ { + shellMainThread.execute { + finishCallback.onTransitionFinished(it) + } + } + ) + } + + override fun onTransitionConsumed( + transition: IBinder, + aborted: Boolean, + finishTransaction: Transaction? + ) { + val pending: PendingTransition? = findPending(transition) + if (pending != null) { + pendingTransitions.remove(pending) + updateTaskStackStates(pending.transaction.getTaskStackStates()) + // Still update the surface order because this means wm didn't lead to any change + if (finishTransaction != null) { + reorderLeashes(finishTransaction) + } + } + autoTransitionHandlerDelegate?.onTransitionConsumed( + transition, + pending?.transaction?.getTaskStackStates() ?: mapOf(), + aborted, + finishTransaction + ) + } + + private fun reorderLeashes(transaction: SurfaceControl.Transaction) { + taskStackStateMap.forEach { (taskId, taskStackState) -> + taskStackMap[taskId]?.let { taskStack -> + mTaskStackStateTranslator.reorderLeash(taskStack, taskStackState, transaction) + } ?: Slog.w(TAG, "Warning: AutoTaskStack with id $taskId not found.") + } + } + + private fun findPending(claimed: IBinder) = pendingTransitions.find { it.isClaimed == claimed } + + private fun startTransitionNow(pending: PendingTransition): IBinder { + val claimedTransition = transitions.startTransition(pending.mType, pending.wct, this) + pending.isClaimed = claimedTransition + pendingTransitions.add(pending) + return claimedTransition + } + + fun startNextTransition() { + if (pendingTransitions.isEmpty()) return + val pending: PendingTransition = pendingTransitions[0] + if (pending.isClaimed != null) { + // Wait for this to start animating. + return + } + pending.isClaimed = transitions.startTransition(pending.mType, pending.wct, this) + } + + internal class PendingTransition( + @field:WindowManager.TransitionType @param:WindowManager.TransitionType val mType: Int, + val wct: WindowContainerTransaction, + val transaction: AutoTaskStackTransaction, + ) { + var isClaimed: IBinder? = null + } + +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt new file mode 100644 index 000000000000..9d121b144492 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/RootTaskStackListener.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.automotive + +import com.android.wm.shell.ShellTaskOrganizer + +/** + * A [TaskListener] which simplifies the interface when used for + * [ShellTaskOrganizer.createRootTask]. + * + * [onRootTaskStackCreated], [onRootTaskStackInfoChanged], [onRootTaskStackDestroyed] will be called + * for the underlying root task. + * The [onTaskAppeared], [onTaskInfoChanged], [onTaskVanished] are called for the children tasks. + */ +interface RootTaskStackListener : ShellTaskOrganizer.TaskListener { + fun onRootTaskStackCreated(rootTaskStack: RootTaskStack) + fun onRootTaskStackInfoChanged(rootTaskStack: RootTaskStack) + fun onRootTaskStackDestroyed(rootTaskStack: RootTaskStack) +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index 4569cf31dab1..b9fccc1c4147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -106,11 +106,12 @@ public class BackAnimationRunner { private Runnable mFinishedCallback; private RemoteAnimationTarget[] mApps; - private IRemoteAnimationFinishedCallback mRemoteCallback; + private RemoteAnimationFinishedStub mRemoteCallback; private static class RemoteAnimationFinishedStub extends IRemoteAnimationFinishedCallback.Stub { //the binder callback should not hold strong reference to it to avoid memory leak. - private WeakReference<BackAnimationRunner> mRunnerRef; + private final WeakReference<BackAnimationRunner> mRunnerRef; + private boolean mAbandoned; private RemoteAnimationFinishedStub(BackAnimationRunner runner) { mRunnerRef = new WeakReference<>(runner); @@ -118,23 +119,29 @@ public class BackAnimationRunner { @Override public void onAnimationFinished() { - BackAnimationRunner runner = mRunnerRef.get(); + synchronized (this) { + if (mAbandoned) { + return; + } + } + final BackAnimationRunner runner = mRunnerRef.get(); if (runner == null) { return; } - if (runner.shouldMonitorCUJ(runner.mApps)) { - InteractionJankMonitor.getInstance().end(runner.mCujType); - } + runner.onAnimationFinish(this); + } - runner.mFinishedCallback.run(); - for (int i = runner.mApps.length - 1; i >= 0; --i) { - SurfaceControl sc = runner.mApps[i].leash; - if (sc != null && sc.isValid()) { - sc.release(); - } + void abandon() { + synchronized (this) { + mAbandoned = true; + final BackAnimationRunner runner = mRunnerRef.get(); + if (runner == null) { + return; + } + if (runner.shouldMonitorCUJ(runner.mApps)) { + InteractionJankMonitor.getInstance().end(runner.mCujType); + } } - runner.mApps = null; - runner.mFinishedCallback = null; } } @@ -144,13 +151,16 @@ public class BackAnimationRunner { */ void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { - InteractionJankMonitor interactionJankMonitor = InteractionJankMonitor.getInstance(); + if (mRemoteCallback != null) { + mRemoteCallback.abandon(); + mRemoteCallback = null; + } + mRemoteCallback = new RemoteAnimationFinishedStub(this); mFinishedCallback = finishedCallback; mApps = apps; - if (mRemoteCallback == null) mRemoteCallback = new RemoteAnimationFinishedStub(this); mWaitingAnimation = false; if (shouldMonitorCUJ(apps)) { - interactionJankMonitor.begin( + InteractionJankMonitor.getInstance().begin( apps[0].leash, mContext, mHandler, mCujType); } try { @@ -161,6 +171,28 @@ public class BackAnimationRunner { } } + void onAnimationFinish(RemoteAnimationFinishedStub finished) { + mHandler.post(() -> { + if (mRemoteCallback != null && finished != mRemoteCallback) { + return; + } + if (shouldMonitorCUJ(mApps)) { + InteractionJankMonitor.getInstance().end(mCujType); + } + + mFinishedCallback.run(); + for (int i = mApps.length - 1; i >= 0; --i) { + final SurfaceControl sc = mApps[i].leash; + if (sc != null && sc.isValid()) { + sc.release(); + } + } + mApps = null; + mFinishedCallback = null; + mRemoteCallback = null; + }); + } + @VisibleForTesting boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { return apps.length > 0 && mCujType != NO_CUJ; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index b796b411dd1a..1323fe0fa9ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; @@ -60,7 +59,6 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; @@ -73,6 +71,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -198,9 +197,6 @@ public class CompatUIController implements OnDisplaysChangedListener, private final CompatUIStatusManager mCompatUIStatusManager; @NonNull - private final FocusTransitionObserver mFocusTransitionObserver; - - @NonNull private final Optional<DesktopUserRepositories> mDesktopUserRepositories; public CompatUIController(@NonNull Context context, @@ -217,8 +213,7 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, @NonNull AccessibilityManager accessibilityManager, @NonNull CompatUIStatusManager compatUIStatusManager, - @NonNull Optional<DesktopUserRepositories> desktopUserRepositories, - @NonNull FocusTransitionObserver focusTransitionObserver) { + @NonNull Optional<DesktopUserRepositories> desktopUserRepositories) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -235,7 +230,6 @@ public class CompatUIController implements OnDisplaysChangedListener, DISAPPEAR_DELAY_MS, flags); mCompatUIStatusManager = compatUIStatusManager; mDesktopUserRepositories = desktopUserRepositories; - mFocusTransitionObserver = focusTransitionObserver; shellInit.addInitCallback(this::onInit, this); } @@ -412,8 +406,7 @@ public class CompatUIController implements OnDisplaysChangedListener, // start tracking the buttons visibility for this task. if (mTopActivityTaskId != taskInfo.taskId && !taskInfo.isTopActivityTransparent - && taskInfo.isVisible - && mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)) { + && taskInfo.isVisible && taskInfo.isFocused) { mTopActivityTaskId = taskInfo.taskId; setHasShownUserAspectRatioSettingsButton(false); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 2600bcc18f72..23a0f4adb6d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -272,8 +272,7 @@ public abstract class WMShellBaseModule { @NonNull CompatUIState compatUIState, @NonNull CompatUIComponentIdGenerator componentIdGenerator, @NonNull CompatUIComponentFactory compatUIComponentFactory, - CompatUIStatusManager compatUIStatusManager, - @NonNull FocusTransitionObserver focusTransitionObserver) { + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } @@ -298,8 +297,7 @@ public abstract class WMShellBaseModule { compatUIShellCommandHandler.get(), accessibilityManager.get(), compatUIStatusManager, - desktopUserRepositories, - focusTransitionObserver)); + desktopUserRepositories)); } @WMSingleton diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 636549fa0662..a6f8150ffc55 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -176,7 +176,6 @@ open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransitio * transition */ @Ignore("TODO(b/356277166): enable the tablet test") - @Postsubmit @Test open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { assumeTrue(tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt index 4987ab7b9344..d65f158e00d6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt @@ -78,7 +78,6 @@ class BottomHalfEnterPipToOtherOrientation(flicker: LegacyFlickerTest) : } @Ignore("TODO(b/356277166): enable the tablet test") - @Presubmit @Test override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { // Test app and pip app should covers the entire screen on start. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 784e1907894d..b5c9fa151dac 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -62,11 +62,12 @@ import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; +import java.util.Optional; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -76,8 +77,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for {@link CompatUIController}. * @@ -128,8 +127,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private DesktopUserRepositories mDesktopUserRepositories; @Mock private DesktopRepository mDesktopRepository; - @Mock - private FocusTransitionObserver mFocusTransitionObserver; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -165,8 +162,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, - mCompatUIStatusManager, Optional.of(mDesktopUserRepositories), - mFocusTransitionObserver) { + mCompatUIStatusManager, Optional.of(mDesktopUserRepositories)) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -284,7 +280,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); @@ -416,7 +411,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while IME is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -449,7 +443,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while keyguard is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -530,7 +523,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutRecreatedIfNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(true).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -546,7 +538,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutNotRecreatedIfNotNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(false).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -567,8 +558,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -584,8 +574,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate task being shown mController.updateActiveTaskInfo(taskInfo); @@ -603,8 +592,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create visible but NOT focused task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ true, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -616,8 +604,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create focused but NOT visible task final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ false, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo2); @@ -629,8 +616,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create NOT focused but NOT visible task final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ false, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo3); @@ -646,8 +632,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_sameTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -675,8 +660,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_transparentTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -694,8 +678,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create transparent task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true, /* isTopActivityTransparent */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -711,7 +694,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -725,7 +707,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -744,7 +725,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() { when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -759,22 +739,23 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, - /* isTopActivityTransparent */ false); + /* isFocused */ false, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible) { + boolean isVisible, boolean isFocused) { return createTaskInfo(displayId, taskId, hasSizeCompat, - isVisible, /* isTopActivityTransparent */ false); + isVisible, isFocused, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible, boolean isTopActivityTransparent) { + boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.isVisible = isVisible; + taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index 9f3c34575b94..81d9d81c4f58 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -74,6 +74,7 @@ public abstract class AppFunctionService extends Service { /* context= */ this, /* onExecuteFunction= */ (platformRequest, callingPackage, + callingPackageSigningInfo, cancellationSignal, callback) -> { AppFunctionService.this.onExecuteFunction( @@ -105,15 +106,17 @@ public abstract class AppFunctionService extends Service { /** * Called by the system to execute a specific app function. * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. + * <p>This method is the entry point for handling all app function requests in an app. When the + * system needs your AppFunctionService to perform a function, it will invoke this method. * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. + * <p>Each function you've registered is identified by a unique identifier. This identifier + * doesn't need to be globally unique, but it must be unique within your app. For example, a + * function to order food could be identified as "orderFood". In most cases, this identifier is + * automatically generated by the AppFunctions SDK. + * + * <p>You can determine which function to execute by calling {@link + * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the + * incoming request to the appropriate logic for handling the specific function. * * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker * thread and dispatch the result with the given callback. You should always report back the @@ -132,7 +135,5 @@ public abstract class AppFunctionService extends Service { @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull - OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> - callback); + @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback); } diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index 707577d6d075..c5095c1a0704 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -354,6 +354,7 @@ static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, j typeface->fStyle = minikin::FontStyle(&reader); typeface->fAPIStyle = reader.read<Typeface::Style>(); typeface->fBaseWeight = reader.read<int>(); + typeface->fIsVariationInstance = false; faceHandles.push_back(toJLong(typeface)); } const jlongArray result = env->NewLongArray(typefaceCount); diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 859cc57dea9f..4c9656792dac 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -20,6 +20,7 @@ #include <private/performance_hint_private.h> #include <future> +#include <memory> #include <optional> #include <vector> diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING index e52e0b16eca3..6a21496f1165 100644 --- a/media/TEST_MAPPING +++ b/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "mediaroutertest" diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index afece5fac0fb..40a786ed560b 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -81,7 +81,9 @@ <androidx.recyclerview.widget.RecyclerView android:id="@+id/device_list" android:layout_width="match_parent" - android:layout_height="200dp" + android:layout_height="wrap_content" + app:layout_constraintHeight_max="220dp" + app:layout_constraintHeight_min="200dp" android:scrollbars="vertical" android:visibility="gone" /> diff --git a/packages/SettingsLib/DataStore/OWNERS b/packages/SettingsLib/DataStore/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/DataStore/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Graph/OWNERS b/packages/SettingsLib/Graph/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Graph/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt index 1ed814a2ae20..51813a1c9aab 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt @@ -69,7 +69,6 @@ constructor( val visitedScreens: Set<String> = setOf(), val locale: Locale? = null, val flags: Int = PreferenceGetterFlags.ALL, - val includeValue: Boolean = true, // TODO: clean up val includeValueDescriptor: Boolean = true, ) diff --git a/packages/SettingsLib/Ipc/OWNERS b/packages/SettingsLib/Ipc/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Ipc/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Metadata/OWNERS b/packages/SettingsLib/Metadata/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Metadata/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/OWNERS_catalyst b/packages/SettingsLib/OWNERS_catalyst new file mode 100644 index 000000000000..d44ac68585a2 --- /dev/null +++ b/packages/SettingsLib/OWNERS_catalyst @@ -0,0 +1,9 @@ +# OWNERS of Catalyst libraries (DataStore, Metadata, etc.) + +# Main developers +jiannan@google.com +cechkahn@google.com +sunnyshao@google.com + +# Emergency only +cipson@google.com diff --git a/packages/SettingsLib/Preference/OWNERS b/packages/SettingsLib/Preference/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Preference/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/Service/OWNERS b/packages/SettingsLib/Service/OWNERS new file mode 100644 index 000000000000..1219dc4aa606 --- /dev/null +++ b/packages/SettingsLib/Service/OWNERS @@ -0,0 +1 @@ +include ../OWNERS_catalyst diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index 7516d2e6ab1b..e3d7902f34b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER; import android.app.admin.DevicePolicyManager; +import android.app.supervision.SupervisionManager; import android.content.ComponentName; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; @@ -59,12 +60,18 @@ public final class ActionDisabledByAdminControllerFactory { } private static boolean isSupervisedDevice(Context context) { - DevicePolicyManager devicePolicyManager = - context.getSystemService(DevicePolicyManager.class); - ComponentName supervisionComponent = - devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( - new UserHandle(UserHandle.myUserId())); - return supervisionComponent != null; + if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { + SupervisionManager supervisionManager = + context.getSystemService(SupervisionManager.class); + return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId()); + } else { + DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + ComponentName supervisionComponent = + devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( + new UserHandle(UserHandle.myUserId())); + return supervisionComponent != null; + } } /** diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 4125a81f9bbc..fc61b1e875f3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -46,6 +46,7 @@ public class GlobalSettings { Settings.Global.APP_AUTO_RESTRICTION_ENABLED, Settings.Global.AUTO_TIME, Settings.Global.AUTO_TIME_ZONE, + Settings.Global.TIME_ZONE_NOTIFICATIONS, Settings.Global.POWER_SOUNDS_ENABLED, Settings.Global.DOCK_SOUNDS_ENABLED, Settings.Global.CHARGING_SOUNDS_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 1fc1f05ae149..dd28402d705f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -91,6 +91,7 @@ public class SecureSettings { Settings.Secure.KEY_REPEAT_TIMEOUT_MS, Settings.Secure.KEY_REPEAT_DELAY_MS, Settings.Secure.CAMERA_GESTURE_DISABLED, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 32d4580f67ec..c0e266fa269f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -102,6 +102,7 @@ public class GlobalSettingsValidators { }); VALIDATORS.put(Global.AUTO_TIME, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.AUTO_TIME_ZONE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.TIME_ZONE_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.POWER_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.DOCK_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index d0e88d5d6a3c..b01f6229af16 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -140,6 +140,8 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.KEY_REPEAT_TIMEOUT_MS, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.KEY_REPEAT_DELAY_MS, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.CAMERA_GESTURE_DISABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put( + Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_LARGE_POINTER_ICON, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index ef0bc3b100e0..c1c3e04d46fd 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -212,10 +212,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final String ERROR_IO_EXCEPTION = "io_exception"; private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG = "failed_to_restore_softap_config"; - private static final String ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES = - "failed_to_convert_network_policies"; - private static final String ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION = - "unknown_backup_serialization_version"; + private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG = + "failed_to_restore_wifi_config"; // Name of the temporary file we use during full backup/restore. This is @@ -1438,7 +1436,6 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { out.writeInt(NETWORK_POLICIES_BACKUP_VERSION); out.writeInt(policies.length); - int numberOfPoliciesBackedUp = 0; for (NetworkPolicy policy : policies) { // We purposefully only backup policies that the user has // defined; any inferred policies might include @@ -1448,30 +1445,26 @@ public class SettingsBackupAgent extends BackupAgentHelper { out.writeByte(BackupUtils.NOT_NULL); out.writeInt(marshaledPolicy.length); out.write(marshaledPolicy); - if (areAgentMetricsEnabled) { - numberOfPoliciesBackedUp++; - } } else { out.writeByte(BackupUtils.NULL); } } - if (areAgentMetricsEnabled) { - numberOfSettingsPerKey.put(KEY_NETWORK_POLICIES, numberOfPoliciesBackedUp); - } } catch (IOException ioe) { Log.e(TAG, "Failed to convert NetworkPolicies to byte array " + ioe.getMessage()); baos.reset(); - mBackupRestoreEventLogger.logItemsBackupFailed( - KEY_NETWORK_POLICIES, - policies.length, - ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } return baos.toByteArray(); } - private byte[] getNewWifiConfigData() { - return mWifiManager.retrieveBackupData(); + @VisibleForTesting + byte[] getNewWifiConfigData() { + byte[] data = mWifiManager.retrieveBackupData(); + if (areAgentMetricsEnabled) { + // We're unable to determine how many settings this includes, so we'll just log 1. + numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1); + } + return data; } private byte[] getLocaleSettings() { @@ -1483,11 +1476,22 @@ public class SettingsBackupAgent extends BackupAgentHelper { return localeList.toLanguageTags().getBytes(); } - private void restoreNewWifiConfigData(byte[] bytes) { + @VisibleForTesting + void restoreNewWifiConfigData(byte[] bytes) { if (DEBUG_BACKUP) { Log.v(TAG, "Applying restored wifi data"); } - mWifiManager.restoreBackupData(bytes); + if (areAgentMetricsEnabled) { + try { + mWifiManager.restoreBackupData(bytes); + mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1); + } catch (Exception e) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG); + } + } else { + mWifiManager.restoreBackupData(bytes); + } } private void restoreNetworkPolicies(byte[] data) { @@ -1498,10 +1502,6 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { int version = in.readInt(); if (version < 1 || version > NETWORK_POLICIES_BACKUP_VERSION) { - mBackupRestoreEventLogger.logItemsRestoreFailed( - KEY_NETWORK_POLICIES, - /* count= */ 1, - ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION); throw new BackupUtils.BadVersionException( "Unknown Backup Serialization Version"); } @@ -1518,15 +1518,10 @@ public class SettingsBackupAgent extends BackupAgentHelper { } // Only set the policies if there was no error in the restore operation networkPolicyManager.setNetworkPolicies(policies); - mBackupRestoreEventLogger.logItemsRestored(KEY_NETWORK_POLICIES, policies.length); } catch (NullPointerException | IOException | BackupUtils.BadVersionException | DateTimeException e) { // NPE can be thrown when trying to instantiate a NetworkPolicy Log.e(TAG, "Failed to convert byte array to NetworkPolicies " + e.getMessage()); - mBackupRestoreEventLogger.logItemsRestoreFailed( - KEY_NETWORK_POLICIES, - /* count= */ 1, - ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index dedd7ebd1ef7..5ad4b8a6dffe 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1715,6 +1715,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, SecureSettingsProto.Accessibility.ENABLED_ACCESSIBILITY_SERVICES); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, + SecureSettingsProto.Accessibility.AUTOCLICK_CURSOR_AREA_SIZE); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, SecureSettingsProto.Accessibility.AUTOCLICK_ENABLED); dumpSetting(s, p, diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java index 95dd0db40c0e..6e5b602c02c5 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java @@ -16,6 +16,7 @@ package com.android.providers.settings; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG; import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG; import static junit.framework.Assert.assertEquals; @@ -28,6 +29,8 @@ import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import android.annotation.Nullable; @@ -69,6 +72,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -834,6 +838,74 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest)); } + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getSuccessCount(), 1); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getFailCount(), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest)); + } + private byte[] generateBackupData(Map<String, String> keyValueData) { int totalBytes = 0; for (String key : keyValueData.keySet()) { diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 5f1f588bb2b5..c7d6e8aed3b4 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope @@ -168,6 +169,12 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() + private var trackWheelScroll: SuspendingPointerInputModifierNode? = null + set(value) { + field?.let { undelegate(it) } + field = value?.also { delegate(it) } + } + private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } @@ -189,6 +196,7 @@ private class NestedDraggableNode( * This is use to track the started position of a drag started on a nested scrollable. */ private var lastFirstDown: Offset? = null + private var lastEventWasScrollWheel: Boolean = false /** The pointers currently down, in order of which they were done and mapping to their type. */ private val pointersDown = linkedMapOf<PointerId, PointerType>() @@ -218,8 +226,11 @@ private class NestedDraggableNode( nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null - if (!enabled && trackDownPositionDelegate != null) { + if (!enabled && trackWheelScroll != null) { + check(trackDownPositionDelegate != null) check(detectDragsDelegate != null) + + trackWheelScroll = null trackDownPositionDelegate = null detectDragsDelegate = null } @@ -232,17 +243,22 @@ private class NestedDraggableNode( ) { if (!enabled) return - if (trackDownPositionDelegate == null) { + if (trackWheelScroll == null) { + check(trackDownPositionDelegate == null) check(detectDragsDelegate == null) + + trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() } trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } } + checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { + trackWheelScroll?.onCancelPointerInput() trackDownPositionDelegate?.onCancelPointerInput() detectDragsDelegate?.onCancelPointerInput() } @@ -457,6 +473,13 @@ private class NestedDraggableNode( * =============================== */ + private suspend fun PointerInputScope.trackWheelScroll() { + awaitEachGesture { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + lastEventWasScrollWheel = event.type == PointerEventType.Scroll + } + } + private suspend fun PointerInputScope.trackDownPosition() { awaitEachGesture { try { @@ -501,7 +524,12 @@ private class NestedDraggableNode( } val sign = offset.sign - if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) { + if ( + nestedScrollController == null && + // TODO(b/388231324): Remove this. + !lastEventWasScrollWheel && + draggable.shouldConsumeNestedScroll(sign) + ) { val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" } // TODO(b/382665591): Ensure that there is at least one pointer down. diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml index 28f80d4af265..7c721b97ee47 100644 --- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml +++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml @@ -15,6 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.compose.core.tests" > <application> @@ -23,7 +24,8 @@ <activity android:name="androidx.activity.ComponentActivity" android:theme="@android:style/Theme.DeviceDefault.DayNight" - android:exported="true" /> + android:exported="true" + tools:replace="android:theme" /> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 7f70e97411f4..f9cf495d9d9f 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -18,6 +18,8 @@ package com.android.compose.gesture import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -35,6 +37,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot @@ -710,6 +713,33 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw rule.onRoot().performTouchInput { down(center) } } + @Test + // TODO(b/388231324): Remove this. + fun nestedScrollWithMouseWheelIsIgnored() { + val draggable = TestDraggable() + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performMouseInput { + enter(center) + scroll( + touchSlop + 1f, + when (orientation) { + Orientation.Horizontal -> ScrollWheel.Horizontal + Orientation.Vertical -> ScrollWheel.Vertical + }, + ) + } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 6bb579d18bf9..b41c55858c75 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -60,17 +60,12 @@ internal interface DragController { * Stop the current drag with the given [velocity]. * * @param velocity The velocity of the drag when it stopped. - * @param canChangeContent Whether the content can be changed as a result of this drag. * @return the consumed [velocity] when the animation complete */ - suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float + suspend fun onStop(velocity: Float): Float - /** - * Cancels the current drag. - * - * @param canChangeContent Whether the content can be changed as a result of this drag. - */ - fun onCancel(canChangeContent: Boolean) + /** Cancels the current drag. */ + fun onCancel() } internal class DraggableHandlerImpl( @@ -295,17 +290,16 @@ private class DragControllerImpl( return newOffset - previousOffset } - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { + override suspend fun onStop(velocity: Float): Float { // To ensure that any ongoing animation completes gracefully and avoids an undefined state, // we execute the actual `onStop` logic in a non-cancellable context. This prevents the // coroutine from being cancelled prematurely, which could interrupt the animation. // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags. - return withContext(NonCancellable) { onStop(velocity, canChangeContent, swipeAnimation) } + return withContext(NonCancellable) { onStop(velocity, swipeAnimation) } } private suspend fun <T : ContentKey> onStop( velocity: Float, - canChangeContent: Boolean, // Important: Make sure that this has the same name as [this.swipeAnimation] so that all the // code here references the current animation when [onDragStopped] is called, otherwise the @@ -319,35 +313,27 @@ private class DragControllerImpl( } val fromContent = swipeAnimation.fromContent + // If we are halfway between two contents, we check what the target will be based on + // the velocity and offset of the transition, then we launch the animation. + + val toContent = swipeAnimation.toContent + + // Compute the destination content (and therefore offset) to settle in. + val offset = swipeAnimation.dragOffset + val distance = swipeAnimation.distance() val targetContent = - if (canChangeContent) { - // If we are halfway between two contents, we check what the target will be based on - // the velocity and offset of the transition, then we launch the animation. - - val toContent = swipeAnimation.toContent - - // Compute the destination content (and therefore offset) to settle in. - val offset = swipeAnimation.dragOffset - val distance = swipeAnimation.distance() - if ( - distance != DistanceUnspecified && - shouldCommitSwipe( - offset = offset, - distance = distance, - velocity = velocity, - wasCommitted = swipeAnimation.currentContent == toContent, - requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, - ) - ) { - toContent - } else { - fromContent - } + if ( + distance != DistanceUnspecified && + shouldCommitSwipe( + offset = offset, + distance = distance, + velocity = velocity, + wasCommitted = swipeAnimation.currentContent == toContent, + requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, + ) + ) { + toContent } else { - // We are doing an overscroll preview animation between scenes. - check(fromContent == swipeAnimation.currentContent) { - "canChangeContent is false but currentContent != fromContent" - } fromContent } @@ -423,10 +409,8 @@ private class DragControllerImpl( } } - override fun onCancel(canChangeContent: Boolean) { - swipeAnimation.contentTransition.coroutineScope.launch { - onStop(velocity = 0f, canChangeContent = canChangeContent) - } + override fun onCancel() { + swipeAnimation.contentTransition.coroutineScope.launch { onStop(velocity = 0f) } } } @@ -519,11 +503,11 @@ private fun scrollController( } override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { - return dragController.onStop(velocity = initialVelocity, canChangeContent = true) + return dragController.onStop(velocity = initialVelocity) } override fun onCancel() { - dragController.onCancel(canChangeContent = true) + dragController.onCancel() } /** @@ -547,9 +531,9 @@ internal const val OffsetVisibilityThreshold = 0.5f private object NoOpDragController : DragController { override fun onDrag(delta: Float) = 0f - override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f + override suspend fun onStop(velocity: Float) = 0f - override fun onCancel(canChangeContent: Boolean) { + override fun onCancel() { /* do nothing */ } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index f5f01d4d1a35..89320f1303e5 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -307,13 +307,13 @@ internal class MultiPointerDraggableNode( velocityTracker.calculateVelocity(maxVelocity) } .toFloat(), - onFling = { controller.onStop(it, canChangeContent = true) }, + onFling = { controller.onStop(it) }, ) }, onDragCancel = { controller -> startFlingGesture( initialVelocity = 0f, - onFling = { controller.onStop(it, canChangeContent = true) }, + onFling = { controller.onStop(it) }, ) }, swipeDetector = swipeDetector, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 5a35d11c0b29..dbac62ffb713 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -247,32 +247,26 @@ class DraggableHandlerTest { suspend fun DragController.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, onAnimationEnd: (Float) -> Unit, ) { - val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene) + val velocityConsumed = onDragStoppedAnimateLater(velocity) onAnimationStart() onAnimationEnd(velocityConsumed.await()) } suspend fun DragController.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, ) = onDragStoppedAnimateNow( velocity = velocity, - canChangeScene = canChangeScene, onAnimationStart = onAnimationStart, onAnimationEnd = {}, ) - fun DragController.onDragStoppedAnimateLater( - velocity: Float, - canChangeScene: Boolean = true, - ): Deferred<Float> { - val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) } + fun DragController.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> { + val velocityConsumed = testScope.async { onStop(velocity) } testScope.testScheduler.runCurrent() return velocityConsumed } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index 4153350fce60..5c6f91bb0e90 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -72,12 +72,12 @@ class MultiPointerDraggableTest { return delta } - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { + override suspend fun onStop(velocity: Float): Float { onStop.invoke(velocity) return velocity } - override fun onCancel(canChangeContent: Boolean) { + override fun onCancel() { error("MultiPointerDraggable never calls onCancel()") } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 7c88d76f28bd..183e4d6f624b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.os.SystemClock import android.view.KeyEvent import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON @@ -540,11 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to full screen", - ), + simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), simpleShortcutCategory( MultiTasking, "Split screen", @@ -704,7 +701,7 @@ object TestShortcuts { android.view.KeyEvent( /* downTime = */ SystemClock.uptimeMillis(), /* eventTime = */ SystemClock.uptimeMillis(), - /* action = */ ACTION_DOWN, + /* action = */ ACTION_UP, /* code = */ KEYCODE_A, /* repeat = */ 0, /* metaState = */ 0, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index 755c218f6789..d9d34f5ace7b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -92,13 +92,14 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - assertThat(uiState).isEqualTo( - AddShortcutDialog( - shortcutLabel = "Standard shortcut", - defaultCustomShortcutModifierKey = - ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + assertThat(uiState) + .isEqualTo( + AddShortcutDialog( + shortcutLabel = "Standard shortcut", + defaultCustomShortcutModifierKey = + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ) ) - ) } } @@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } @@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - assertThat((uiState as AddShortcutDialog).errorMessage) - .isEmpty() + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() } } @@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() { + fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) assertThat(isHandled).isTrue() } } @Test - fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { + fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed) assertThat(isHandled).isFalse() } } @Test - fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() { + fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { // Close the dialog and show it again viewModel.onDialogDismissed() viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } private suspend fun openAddShortcutDialogAndSetShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) viewModel.onSetShortcut() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index b3417b9de36d..c44f27ef348b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -46,8 +46,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -76,7 +74,6 @@ class KeyguardInteractorTest : SysuiTestCase() { private val configRepository by lazy { kosmos.fakeConfigurationRepository } private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository } private val shadeRepository by lazy { kosmos.shadeRepository } - private val powerInteractor by lazy { kosmos.powerInteractor } private val keyguardRepository by lazy { kosmos.keyguardRepository } private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } @@ -444,7 +441,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() advanceTimeBy(1000L) assertThat(isAbleToDream).isEqualTo(false) @@ -460,9 +456,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() - runCurrent() - // After some delay, still false advanceTimeBy(300L) assertThat(isAbleToDream).isEqualTo(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt index b0698555941c..98e3c68e6e33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -406,4 +407,48 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { // It should not have any effect. assertEquals(listOf(false, true, false, true), canWake) } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testCanWakeDirectlyToGone_falseAsSoonAsTransitionsAwayFromGone() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false // Defaults to false. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + assertEquals( + listOf( + false, + true, // Because we're GONE. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + assertEquals( + listOf( + false, + true, + false, // False as soon as we start a transition away from GONE. + ), + canWake, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index feaf06aca29a..ade7614ae853 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -16,10 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -72,6 +75,28 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsOpen() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + step(0.2f), + step(0.3f), + step(1f), + ), + testScope, + ) + + values.forEach { assertThat(it).isEqualTo(1f) } + } + + @Test fun blurRadiusGoesToMaximumWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -88,6 +113,25 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt index d909c5ab5f1b..914094fa39df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -153,7 +155,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusIsMaxWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -170,7 +172,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -185,6 +187,44 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza ) } + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = 1.0f, + endValue = 1.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index fc1d73b62abd..3a3f5371d195 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.Dialog; import android.media.projection.StopReason; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.flags.QsDetailedView; import com.android.systemui.qs.flags.QsInCompose; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; @@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; + @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest @@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + QsDetailedView.FLAG_NAME); } @Mock @@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase { .notifyPermissionRequestDisplayed(mContext.getUserId()); } + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testNotStartingAndRecording_returnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNotNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testStarting_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testRecording_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + mTile.getDetailsViewModel(Assert::assertNull); + } + private QSTile.Icon createExpectedIcon(int resId) { if (QsInCompose.isEnabled()) { return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 789ca5158dbf..62c360400582 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -85,7 +85,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; @@ -335,16 +334,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); mMainDispatcher = getMainDispatcher(); - KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps = - KeyguardInteractorFactory.create(); - mFakeKeyguardRepository = keyguardInteractorDeps.getRepository(); + mFakeKeyguardRepository = mKosmos.getKeyguardRepository(); mFakeKeyguardClockRepository = new FakeKeyguardClockRepository(); mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor(); - mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor(); + mKeyguardInteractor = mKosmos.getKeyguardInteractor(); mShadeRepository = new FakeShadeRepository(); mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl( new ShadeAnimationRepository(), mShadeRepository); - mPowerInteractor = keyguardInteractorDeps.getPowerInteractor(); + mPowerInteractor = mKosmos.getPowerInteractor(); when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn( MutableStateFlow(false)); when(mKeyguardTransitionInteractor.isInTransition(any(), any())) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt index bb9141afe404..5f73ac45d12f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt @@ -45,8 +45,11 @@ import kotlin.test.Test import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mockito.verify +import org.mockito.kotlin.any import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verifyNoMoreInteractions @SmallTest @@ -106,10 +109,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure the legacy code path does not change airplane mode when the refactor // flag is enabled. underTest.setIsAirplaneMode(IconState(true, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) underTest.setIsAirplaneMode(IconState(false, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -144,10 +147,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure changing airplane mode from airplaneModeRepository does nothing // if the StatusBarSignalPolicyRefactor is not enabled. airplaneModeInteractor.setIsAirplaneMode(true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) airplaneModeInteractor.setIsAirplaneMode(false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -196,7 +199,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { underTest.setEthernetIndicators( IconState(/* visible= */ true, /* icon= */ 1, /* contentDescription= */ "Ethernet") ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) underTest.setEthernetIndicators( IconState( @@ -205,7 +208,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { /* contentDescription= */ "No ethernet", ) ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test @@ -217,13 +220,13 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { clearInvocations(statusBarIconController) connectivityRepository.fake.setEthernetConnected(default = true, validated = true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = false, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = true, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt index 912633c874ed..e6fbc725af04 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt @@ -101,7 +101,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is not queried for group or row controllers inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, never()).getGroupController(any()) verify(spyViewRenderer, never()).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -121,7 +120,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -144,7 +142,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -162,7 +159,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listWith2Groups8Entries()) // VERIFY that the listeners are invoked once per group and once per entry - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -182,7 +179,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listOf()) // VERIFY that the stack listener is invoked once but other listeners are not - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -203,8 +200,6 @@ class RenderStageManagerTest : SysuiTestCase() { private class FakeNotifViewRenderer : NotifViewRenderer { override fun onRenderList(notifList: List<ListEntry>) {} - override fun getStackController(): NotifStackController = mock() - override fun getGroupController(group: GroupEntry): NotifGroupController = mock() override fun getRowController(entry: NotificationEntry): NotifRowController = mock() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index 54ce88b40c11..83c61507a506 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -275,7 +275,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -293,7 +292,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -311,7 +309,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 0, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -329,7 +326,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -347,7 +343,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -365,7 +360,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -383,7 +377,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -401,7 +394,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 06b1c432955a..b3a60b052d08 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -37,7 +37,7 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.shadeTestUtil import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter @@ -115,7 +115,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -133,7 +132,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -151,7 +149,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -183,7 +180,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -217,7 +213,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 459778868ccd..a045b37a8119 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition import com.android.systemui.testKosmos +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlin.test.assertIs @@ -1395,6 +1396,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S assertThat(stackAbsoluteBottom).isEqualTo(100F) } + @Test + fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() = + testScope.runTest { + val blurRadius by collectLastValue(underTest.blurRadius) + assertThat(blurRadius).isEqualTo(0.0f) + + kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f + assertThat(blurRadius).isEqualTo(30.0f) + + kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f + assertThat(blurRadius).isEqualTo(40.0f) + } + private suspend fun TestScope.showLockscreen() { shadeTestUtil.setQsExpansion(0f) shadeTestUtil.setLockscreenShadeExpansion(0f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index a9db0b70dd4d..faf736a543dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeHomeStatusBarViewModel( override val operatorNameViewModel: StatusBarOperatorNameViewModel ) : HomeStatusBarViewModel { - private val areNotificationLightsOut = MutableStateFlow(false) + override val areNotificationsLightsOut = MutableStateFlow(false) override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false) @@ -77,14 +77,12 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut - val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK var lightIconTint = Color.WHITE - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = MutableStateFlow( StatusBarTintColor { viewBounds -> if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index e91875cd0885..a70b777a25f2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -22,6 +22,7 @@ import android.app.StatusBarManager.DISABLE_CLOCK import android.app.StatusBarManager.DISABLE_NONE import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS import android.app.StatusBarManager.DISABLE_SYSTEM_INFO +import android.content.testableContext import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -59,7 +60,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.data.model.StatusBarMode -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel @@ -363,7 +363,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isTrue() } @@ -377,7 +377,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -391,7 +391,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -405,7 +405,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -415,7 +415,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { fun areNotificationsLightsOut_requiresFlagEnabled() = kosmos.runTest { assertLogsWtf { - val flow = underTest.areNotificationsLightsOut(DISPLAY_ID) + val flow = underTest.areNotificationsLightsOut assertThat(flow).isEqualTo(emptyFlow<Boolean>()) } } @@ -1005,11 +1005,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_getsDarkTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(1, 1, 3, 3)) @@ -1019,11 +1019,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(6, 6, 7, 7)) @@ -1033,11 +1033,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) var tint = areaTint?.tint(Rect(1, 1, 3, 3)) diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags index 162d8aebfc62..02b2bcf8e40d 100644 --- a/packages/SystemUI/proguard_common.flags +++ b/packages/SystemUI/proguard_common.flags @@ -1,5 +1,11 @@ -include proguard_kotlin.flags --keep class com.android.systemui.VendorServices + +# VendorServices implements CoreStartable and may be instantiated reflectively in +# SystemUIApplication#startAdditionalStartable. +# TODO(b/373579455): Rewrite this to a @UsesReflection keep annotation. +-keep class com.android.systemui.VendorServices { + public void <init>(); +} # Needed to ensure callback field references are kept in their respective # owning classes when the downstream callback registrars only store weak refs. diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml new file mode 100644 index 000000000000..f8c0fa04cd39 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.45561" + android:valueTo="0.69699" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.6288400000000001" + android:valueTo="0.81618" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.69699" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.81618" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="0" + android:valueFrom="90" + android:valueTo="135" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="500" + android:valueFrom="135" + android:valueTo="180" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.05063" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.042350000000000006" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.05063" + android:valueTo="0.06146" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.042350000000000006" + android:valueTo="0.040780000000000004" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1017" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="0" + android:scaleX="0.06146" + android:scaleY="0.040780000000000004" + android:translateX="44" + android:translateY="28"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml new file mode 100644 index 000000000000..6ae89f91c5ee --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="56" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="15.485" + android:valueTo="12.321" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="278" + android:propertyName="translateX" + android:startOffset="56" + android:valueFrom="12.321" + android:valueTo="7.576" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="15.485" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml new file mode 100644 index 000000000000..571f69d51ac4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="367" + android:propertyName="pathData" + android:startOffset="133" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="scaleX" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleY" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleX" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleY" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml new file mode 100644 index 000000000000..f64690268cfe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="7.576" + android:valueTo="15.485" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="7.576" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml new file mode 100644 index 000000000000..aa4e09fa4033 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:height="56dp" + android:width="88dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="43.528999999999996" + android:translateY="27.898" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillColor="#3d90ff" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="pathData" + android:duration="167" + android:startOffset="0" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="pathData" + android:duration="333" + android:startOffset="167" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="scaleX" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleX" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="translateX" + android:duration="517" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 0b624e1687a6..58f2d3ccc6a8 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -44,7 +44,7 @@ app:layout_constraintBottom_toTopOf="@id/volume_dialog_main_slider_container" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent" /> <include android:id="@+id/volume_dialog_main_slider_container" diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml index 967cb3fd68de..6eb7b730e105 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -14,8 +14,9 @@ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height"> + android:layout_width="0dp" + android:layout_height="0dp" + android:maxHeight="@dimen/volume_dialog_slider_height"> <com.google.android.material.slider.Slider android:id="@+id/volume_dialog_slider" diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index e65d0b938b65..6748cfa05c35 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -20,10 +20,9 @@ <ImageButton android:id="@+id/volume_drawer_button" - android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" - android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_width="match_parent" + android:layout_height="match_parent" android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:layout_marginBottom="@dimen/volume_dialog_components_spacing" android:contentDescription="@string/volume_ringer_mode" android:gravity="center" android:tint="@androidprv:color/materialColorOnSurface" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 8bf4e373a6e0..2ffa3d19e161 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1278,6 +1278,7 @@ <dimen name="qs_center_guideline_padding">10dp</dimen> <dimen name="qs_media_action_spacing">4dp</dimen> <dimen name="qs_media_action_margin">12dp</dimen> + <dimen name="qs_media_action_play_pause_width">72dp</dimen> <dimen name="qs_seamless_height">24dp</dimen> <dimen name="qs_seamless_icon_size">12dp</dimen> <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> @@ -2116,6 +2117,11 @@ <dimen name="volume_dialog_button_size">40dp</dimen> <dimen name="volume_dialog_slider_width">52dp</dimen> <dimen name="volume_dialog_slider_height">254dp</dimen> + <!-- + A primary goal of this margin is to vertically constraint slider height in the landscape + orientation when the vertical space is limited + --> + <dimen name="volume_dialog_slider_vertical_margin">124dp</dimen> <fraction name="volume_dialog_half_opened_bias">0.2</fraction> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a01ff3d5258f..d445ed927a25 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3919,6 +3919,16 @@ The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_plus_symbol">+</string> + <!-- Accessibility label for the plus icon on a shortcut in shortcut helper that allows the user + to add a new custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_add_shortcut_button_label">Add shortcut</string> + <!-- Accessibility label for the bin(trash) icon on a shortcut in shortcut helper that allows the + user to delete an existing custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_delete_shortcut_button_label">Delete shortcut</string> <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> diff --git a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml index 9018e5b7ed92..a8f616c2427d 100644 --- a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml @@ -6,10 +6,13 @@ <Constraint android:id="@id/volume_dialog_main_slider_container" android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height" + android:layout_height="0dp" + android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.5" /> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml index 297c38873164..b4d8ae791f36 100644 --- a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml @@ -6,10 +6,13 @@ <Constraint android:id="@id/volume_dialog_main_slider_container" android:layout_width="@dimen/volume_dialog_slider_width" - android:layout_height="@dimen/volume_dialog_slider_height" + android:layout_height="0dp" + android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="@dimen/volume_dialog_slider_height" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="@fraction/volume_dialog_half_opened_bias" /> </ConstraintSet>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 274fa59045d7..a16b4a6892b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -53,11 +53,11 @@ constructor( override suspend fun onActivated(): Nothing { viewModel.shortcutCustomizationUiState.collect { uiState -> - when(uiState){ + when (uiState) { is AddShortcutDialog, is DeleteShortcutDialog, is ResetShortcutDialog -> { - if (dialog == null){ + if (dialog == null) { dialog = createDialog().also { it.show() } } } @@ -85,7 +85,9 @@ constructor( ShortcutCustomizationDialog( uiState = uiState, modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp), - onKeyPress = { viewModel.onKeyPressed(it) }, + onShortcutKeyCombinationSelected = { + viewModel.onShortcutKeyCombinationSelected(it) + }, onCancel = { dialog.dismiss() }, onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } }, onConfirmDeleteShortcut = { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 3819f6d41856..d9e55f89cda5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -65,7 +69,7 @@ import com.android.systemui.res.R fun ShortcutCustomizationDialog( uiState: ShortcutCustomizationUiState, modifier: Modifier = Modifier, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, @@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog( ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { - AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut) + AddShortcutDialog( + modifier, + uiState, + onShortcutKeyCombinationSelected, + onCancel, + onConfirmSetShortcut, + ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut) @@ -91,29 +101,27 @@ fun ShortcutCustomizationDialog( private fun AddShortcutDialog( modifier: Modifier, uiState: ShortcutCustomizationUiState.AddShortcutDialog, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, - onConfirmSetShortcut: () -> Unit -){ + onConfirmSetShortcut: () -> Unit, +) { Column(modifier = modifier) { Title(uiState.shortcutLabel) Description( - text = - stringResource( - id = R.string.shortcut_customize_mode_add_shortcut_description - ) + text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description) ) PromptShortcutModifier( modifier = - Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) - .width(131.dp) - .height(48.dp), + Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) + .width(131.dp) + .height(48.dp), defaultModifierKey = uiState.defaultCustomShortcutModifierKey, ) SelectedKeyCombinationContainer( shouldShowError = uiState.errorMessage.isNotEmpty(), - onKeyPress = onKeyPress, + onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, + onConfirmSetShortcut = onConfirmSetShortcut, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -121,9 +129,7 @@ private fun AddShortcutDialog( isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(), onConfirm = onConfirmSetShortcut, confirmButtonText = - stringResource( - R.string.shortcut_helper_customize_dialog_set_shortcut_button_label - ), + stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label), ) } } @@ -132,20 +138,15 @@ private fun AddShortcutDialog( private fun DeleteShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmDeleteShortcut: () -> Unit -){ + onConfirmDeleteShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), onCancel = onCancel, onConfirm = onConfirmDeleteShortcut, ) @@ -155,20 +156,15 @@ private fun DeleteShortcutDialog( private fun ResetShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmResetShortcut: () -> Unit -){ + onConfirmResetShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), onCancel = onCancel, onConfirm = onConfirmResetShortcut, ) @@ -201,6 +197,9 @@ private fun DialogButtons( onConfirm: () -> Unit, confirmButtonText: String, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + Row( modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp) @@ -218,6 +217,10 @@ private fun DialogButtons( ) Spacer(modifier = Modifier.width(8.dp)) ShortcutHelperButton( + modifier = + Modifier.focusRequester(focusRequester).focusProperties { + canFocus = true + }, // enable focus on touch/click mode onClick = onConfirm, color = MaterialTheme.colorScheme.primary, width = 116.dp, @@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) { @Composable private fun SelectedKeyCombinationContainer( shouldShowError: Boolean, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, + onConfirmSetShortcut: () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() @@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer( Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) - .onKeyEvent { onKeyPress(it) } + .onPreviewKeyEvent { keyEvent -> + val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) + if ( + !keyEventProcessed && + keyEvent.key == Key.Enter && + keyEvent.type == KeyEventType.KeyUp + ) { + onConfirmSetShortcut() + true + } else keyEventProcessed + } .focusProperties { canFocus = true } // enables keyboard focus when in touch mode .focusRequester(focusRequester), interactionSource = interactionSource, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index aea583d67289..ba31d08c9c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -729,6 +729,7 @@ private fun AddShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label), ) } @@ -749,6 +750,7 @@ private fun DeleteShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt index 55c0fe297bcb..9a380f495176 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt @@ -230,6 +230,7 @@ fun ShortcutHelperButton( contentPaddingVertical: Dp = 10.dp, enabled: Boolean = true, border: BorderStroke? = null, + contentDescription: String? = null, ) { ShortcutHelperButtonSurface( onClick = onClick, @@ -254,8 +255,7 @@ fun ShortcutHelperButton( Icon( tint = contentColor, imageVector = iconSource.imageVector, - contentDescription = - null, // TODO this probably should not be null for accessibility. + contentDescription = contentDescription, modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 373eb250d61d..915a66c43a12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -46,6 +46,7 @@ constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, ) { + private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) @@ -94,9 +95,16 @@ constructor( shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) } - fun onKeyPressed(keyEvent: KeyEvent): Boolean { - if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) { - updatePressedKeys(keyEvent) + fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { + if (isModifier(keyEvent)) { + return false + } + if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) { + keyDownEventCache = keyEvent + return true + } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) { + updatePressedKeys(keyDownEventCache!!) + clearKeyDownEventCache() return true } return false @@ -157,16 +165,21 @@ constructor( return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState } + private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key) + private fun updatePressedKeys(keyEvent: KeyEvent) { - val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key) val keyCombination = KeyCombination( modifiers = keyEvent.nativeKeyEvent.modifiers, - keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null, + keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null, ) shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination) } + private fun clearKeyDownEventCache() { + keyDownEventCache = null + } + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 9896365abff9..b42da5265d86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -132,6 +132,8 @@ constructor( if (SceneContainerFlag.isEnabled) return@collect startTransitionTo( toState = KeyguardState.GONE, + modeOnCanceled = TransitionModeOnCanceled.REVERSE, + ownerReason = "canWakeDirectlyToGone = true", ) } else if (shouldTransitionToLockscreen) { val modeOnCanceled = @@ -146,7 +148,7 @@ constructor( startTransitionTo( toState = KeyguardState.LOCKSCREEN, modeOnCanceled = modeOnCanceled, - ownerReason = "listen for aod to awake" + ownerReason = "listen for aod to awake", ) } else if (shouldTransitionToOccluded) { startTransitionTo( 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 fbe31bbf36e6..8f7f2a0a8cbb 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 @@ -44,7 +44,6 @@ 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.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -84,7 +83,6 @@ class KeyguardInteractor @Inject constructor( private val repository: KeyguardRepository, - powerInteractor: PowerInteractor, bouncerRepository: KeyguardBouncerRepository, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, shadeRepository: ShadeRepository, @@ -216,11 +214,7 @@ constructor( // should actually be quite strange to leave AOD and then go straight to // DREAMING so this should be fine. delay(IS_ABLE_TO_DREAM_DELAY_MS) - isDreaming - .sample(powerInteractor.isAwake) { isDreaming, isAwake -> - isDreaming && isAwake - } - .debounce(50L) + isDreaming.debounce(50L) } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index a133f06b3f41..3bdc32dce6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -116,9 +116,10 @@ constructor( * - We're wake and unlocking (fingerprint auth occurred while asleep). * - We're allowed to ignore auth and return to GONE, due to timeouts not elapsing. * - We're DREAMING and dismissible. - * - We're already GONE. Technically you're already awake when GONE, but this makes it easier to - * reason about this state (for example, if canWakeDirectlyToGone, don't tell WM to pause the - * top activity - something you should never do while GONE as well). + * - We're already GONE and not transitioning out of GONE. Technically you're already awake when + * GONE, but this makes it easier to reason about this state (for example, if + * canWakeDirectlyToGone, don't tell WM to pause the top activity - something you should never + * do while GONE as well). */ val canWakeDirectlyToGone = combine( @@ -138,7 +139,8 @@ constructor( canIgnoreAuthAndReturnToGone || (currentState == KeyguardState.DREAMING && keyguardInteractor.isKeyguardDismissible.value) || - currentState == KeyguardState.GONE + (currentState == KeyguardState.GONE && + transitionInteractor.getStartedState() == KeyguardState.GONE) } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt index 542fb9b46bef..3eb8522e0338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -23,4 +23,10 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { // No-op config that will be used by dagger of other SysUI variants which don't blur the // background surface. @Inject constructor() : this(0.0f, 0.0f) + + companion object { + // Blur the shade much lesser than the background surface so that the surface is + // distinguishable from the background. + @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt index e77e9dd9e9ed..eb1afb406d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt @@ -30,6 +30,9 @@ interface PrimaryBouncerTransition { /** Radius of blur applied to the window's root view. */ val windowBlurRadius: Flow<Float> + /** Radius of blur applied to the notifications on expanded shade */ + val notificationBlurRadius: Flow<Float> + fun transitionProgressToBlurRadius( starBlurRadius: Float, endBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index f17455788d6e..92bb5e6029cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -73,7 +75,28 @@ constructor( val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow() - val notificationAlpha: Flow<Float> = alphaFlow + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + alphaFlow + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow<Float>() + } override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt index dbb6a49e7844..e3b55874de6f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt @@ -53,4 +53,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt index d8b617a60129..c937d5c6453d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt @@ -64,4 +64,6 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio }, onFinish = { blurConfig.maxBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt index 597df15a2b55..5ab458334a25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index c373fd01ba20..44c4c8723dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,6 +34,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow /** * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to @@ -70,6 +73,29 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + lockscreenAlpha + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow() + } + override val deviceEntryParentViewAlpha: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 44598107fa4b..4d3e27265cea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt index fab8008cbfa7..224191b64f5f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt index eebdf2ef418e..0f8495f34d22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt @@ -80,4 +80,6 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt index 3636b747d5c9..a13eef2388f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt @@ -43,4 +43,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 4ed3e6cde230..d1233f220f47 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -166,6 +166,9 @@ constructor( createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard) } + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) + val scrimAlpha: Flow<ScrimAlpha> = bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 2edc93cb5617..c53a408a88e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, ), ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index 3a54a26858d4..fe1708efea2f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index 09544827a51a..a6b9442b1270 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -31,6 +31,7 @@ import androidx.media3.session.CommandButton import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -128,7 +129,11 @@ constructor( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -230,17 +235,33 @@ constructor( Player.COMMAND_PLAY_PAUSE -> { if (!controller.isPlaying) { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } else { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 4f9791353b8a..9bf556cf07c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -29,6 +29,7 @@ import android.media.session.PlaybackState import android.service.notification.StatusBarNotification import android.util.Log import androidx.media.utils.MediaConstants +import com.android.systemui.Flags import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -69,7 +70,11 @@ fun createActionsFromState( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -157,18 +162,34 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 3928a711f840..a2ddc20844e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1016,9 +1016,24 @@ constructor( expandedLayout.load(context, R.xml.media_recommendations_expanded) } } + readjustPlayPauseWidth() refreshState() } + private fun readjustPlayPauseWidth() { + // TODO: move to xml file when flag is removed. + if (Flags.mediaControlsUiUpdate()) { + collapsedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + expandedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + } + } + /** Get a view state based on the width and height set by the scene */ private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? { logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index ec8d30b01eab..e93cec875429 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.screenrecord.data.model.ScreenRecordModel; @@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.function.Consumer; + import javax.inject.Inject; /** @@ -122,17 +126,78 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> @Override protected void handleClick(@Nullable Expandable expandable) { + handleClick(() -> showDialog(expandable)); + } + + private void showDialog(@Nullable Expandable expandable) { + final Dialog dialog = mController.createScreenRecordDialog( + this::onStartRecordingClicked); + + executeWhenUnlockedKeyguard(() -> { + // We animate from the touched view only if we are not on the keyguard, given that if we + // are we will dismiss it which will also collapse the shade. + boolean shouldAnimateFromExpandable = + expandable != null && !mKeyguardStateController.isShowing(); + + if (shouldAnimateFromExpandable) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, + controller, /* animateBackgroundBoundsChange= */ true); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + }); + } + + private void onStartRecordingClicked() { + // We dismiss the shade. Since starting the recording will also dismiss the dialog (if + // there is one showing), we disable the exit animation which looks weird when it happens + // at the same time as the shade collapsing. + mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); + mPanelInteractor.collapsePanels(); + } + + private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) { + ActivityStarter.OnDismissAction dismissAction = () -> { + dismissActionCallback.run(); + + int uid = mUserContextProvider.getUserContext().getUserId(); + mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); + + return false; + }; + + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, + true /* afterKeyguardDone */); + } + + private void handleClick(Runnable showPromptCallback) { if (mController.isStarting()) { cancelCountdown(); } else if (mController.isRecording()) { stopRecording(); } else { - mUiHandler.post(() -> showPrompt(expandable)); + mUiHandler.post(showPromptCallback); } refreshState(); } @Override + public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { + handleClick(() -> + callback.accept(new ScreenRecordDetailsViewModel()) + ); + return true; + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { boolean isStarting = mController.isStarting(); boolean isRecording = mController.isRecording(); @@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void showPrompt(@Nullable Expandable expandable) { - // We animate from the touched view only if we are not on the keyguard, given that if we - // are we will dismiss it which will also collapse the shade. - boolean shouldAnimateFromExpandable = - expandable != null && !mKeyguardStateController.isShowing(); - - // Create the recording dialog that will collapse the shade only if we start the recording. - Runnable onStartRecordingClicked = () -> { - // We dismiss the shade. Since starting the recording will also dismiss the dialog, we - // disable the exit animation which looks weird when it happens at the same time as the - // shade collapsing. - mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); - mPanelInteractor.collapsePanels(); - }; - - final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked); - - ActivityStarter.OnDismissAction dismissAction = () -> { - if (shouldAnimateFromExpandable) { - DialogTransitionAnimator.Controller controller = - expandable.dialogTransitionController(new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); - if (controller != null) { - mDialogTransitionAnimator.show(dialog, - controller, /* animateBackgroundBoundsChange= */ true); - } else { - dialog.show(); - } - } else { - dialog.show(); - } - - int uid = mUserContextProvider.getUserContext().getUserId(); - mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); - - return false; - }; - - mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, - true /* afterKeyguardDone */); - } - private void cancelCountdown() { Log.d(TAG, "Cancelling countdown"); mController.cancelCountdown(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt new file mode 100644 index 000000000000..42cb1248ccff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.res.R + +/** The view model used for the screen record details view in the Quick Settings */ +class ScreenRecordDetailsViewModel() : TileDetailsViewModel() { + @Composable + override fun GetContentView() { + // TODO(b/378514312): Finish implementing this function. + AndroidView( + modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT), + factory = { context -> + // Inflate with the existing dialog xml layout + LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) + }, + ) + } + + override fun clickOnSettingsButton() { + // No settings button in this tile. + } + + override fun getTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + return "Screen recording" + } + + override fun getSubTitle(): String { + // No sub-title in this tile. + return "" + } + + companion object { + private val VIEW_MAX_HEIGHT: Dp = 320.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 19152170757c..c4306d3f7530 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -109,6 +109,7 @@ import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -914,13 +915,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!com.android.systemui.Flags.bouncerUiRevamp()) return; if (isBouncerShowing && isExpanded()) { - // Blur the shade much lesser than the background surface so that the surface is - // distinguishable from the background. - float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3; + float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius( + mDepthController.getMaxBlurRadiusPx()); mView.setRenderEffect(RenderEffect.createBlurEffect( shadeBlurEffect, shadeBlurEffect, - Shader.TileMode.MIRROR)); + Shader.TileMode.CLAMP)); } else { mView.setRenderEffect(null); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt index 4d35d0eba178..e358dcec8b10 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayChangeLatencyTracker.kt @@ -24,7 +24,6 @@ import com.android.systemui.common.ui.view.ChoreographerUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.ui.view.WindowRootView -import com.android.systemui.shade.ShadeDisplayChangeLatencyTracker.Companion.TIMEOUT import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.util.kotlin.getOrNull import java.util.Optional @@ -33,7 +32,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter @@ -135,7 +133,7 @@ constructor( private companion object { const val TAG = "ShadeDisplayLatency" - val t = TrackTracer(trackName = TAG) + val t = TrackTracer(trackName = TAG, trackGroup = "shade") val TIMEOUT = 3.seconds const val SHADE_MOVE_ACTION = LatencyTracker.ACTION_SHADE_WINDOW_DISPLAY_CHANGE } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt index 359ddd86f115..5fab889735a6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt @@ -18,13 +18,16 @@ package com.android.systemui.shade import android.annotation.IntDef import android.os.Trace +import android.os.Trace.TRACE_TAG_APP as TRACE_TAG import android.util.Log import androidx.annotation.FloatRange +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.Compile import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -import android.os.Trace.TRACE_TAG_APP as TRACE_TAG /** * A class responsible for managing the notification panel's current state. @@ -38,6 +41,8 @@ class ShadeExpansionStateManager @Inject constructor() { private val expansionListeners = CopyOnWriteArrayList<ShadeExpansionListener>() private val stateListeners = CopyOnWriteArrayList<ShadeStateListener>() + private val stateLogger = TraceStateLogger(trackGroup("shade", TRACK_NAME)) + @PanelState private var state: Int = STATE_CLOSED @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f private var expanded: Boolean = false @@ -75,7 +80,7 @@ class ShadeExpansionStateManager @Inject constructor() { fun onPanelExpansionChanged( @FloatRange(from = 0.0, to = 1.0) fraction: Float, expanded: Boolean, - tracking: Boolean + tracking: Boolean, ) { require(!fraction.isNaN()) { "fraction cannot be NaN" } val oldState = state @@ -113,11 +118,8 @@ class ShadeExpansionStateManager @Inject constructor() { ) if (Trace.isTagEnabled(TRACE_TAG)) { - Trace.traceCounter(TRACE_TAG, "panel_expansion", (fraction * 100).toInt()) - if (state != oldState) { - Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK_NAME, 0) - Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK_NAME, state.panelStateToString(), 0) - } + TrackTracer.instantForGroup("shade", "panel_expansion", fraction) + stateLogger.log(state.panelStateToString()) } val expansionChangeEvent = ShadeExpansionChangeEvent(fraction, expanded, tracking) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt new file mode 100644 index 000000000000..2705cdafb4de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeStateTraceLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.app.tracing.TraceStateLogger +import com.android.app.tracing.TrackGroupUtils.trackGroup +import com.android.app.tracing.coroutines.TrackTracer.Companion.instantForGroup +import com.android.app.tracing.coroutines.launchTraced +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SysUISingleton +class ShadeStateTraceLogger +@Inject +constructor( + private val shadeInteractor: ShadeInteractor, + private val shadeDisplaysRepository: ShadeDisplaysRepository, + @Application private val scope: CoroutineScope, +) : CoreStartable { + override fun start() { + scope.launchTraced("ShadeStateTraceLogger") { + launch { + val stateLogger = createTraceStateLogger("isShadeLayoutWide") + shadeInteractor.isShadeLayoutWide.collect { stateLogger.log(it.toString()) } + } + launch { + val stateLogger = createTraceStateLogger("shadeMode") + shadeInteractor.shadeMode.collect { stateLogger.log(it.toString()) } + } + launch { + shadeInteractor.shadeExpansion.collect { + instantForGroup(TRACK_GROUP_NAME, "shadeExpansion", it) + } + } + launch { + shadeDisplaysRepository.displayId.collect { + instantForGroup(TRACK_GROUP_NAME, "displayId", it) + } + } + } + } + + private fun createTraceStateLogger(trackName: String): TraceStateLogger { + return TraceStateLogger(trackGroup(TRACK_GROUP_NAME, trackName)) + } + + private companion object { + const val TRACK_GROUP_NAME = "shade" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt index a36c56eafbfc..11805992fd6a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -27,7 +27,7 @@ import com.android.app.tracing.coroutines.TrackTracer * them across various threads' logs. */ object ShadeTraceLogger { - private val t = TrackTracer(trackName = "ShadeTraceLogger") + private val t = TrackTracer(trackName = "ShadeTraceLogger", trackGroup = "shade") @JvmStatic fun logOnMovedToDisplay(displayId: Int, config: Configuration) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt index c4de78b8a28e..570a7853c394 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt @@ -40,4 +40,9 @@ internal abstract class StartShadeModule { @IntoMap @ClassKey(ShadeStartable::class) abstract fun provideShadeStartable(startable: ShadeStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(ShadeStateTraceLogger::class) + abstract fun provideShadeStateTraceLogger(startable: ShadeStateTraceLogger): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt index 37989f56d559..2885ce80bda9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt @@ -11,13 +11,13 @@ import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffXfermode import android.graphics.RadialGradient import android.graphics.Shader -import android.os.Trace import android.util.AttributeSet import android.util.MathUtils.lerp import android.view.MotionEvent import android.view.View import android.view.animation.PathInterpolator import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.keyguard.logging.ScrimLogger import com.android.systemui.shade.TouchLogger import com.android.systemui.statusbar.LightRevealEffect.Companion.getPercentPastThreshold @@ -321,9 +321,8 @@ constructor( } revealEffect.setRevealAmountOnScrim(value, this) updateScrimOpaque() - Trace.traceCounter( - Trace.TRACE_TAG_APP, - "light_reveal_amount $logString", + TrackTracer.instantForGroup( + "scrim", { "light_reveal_amount $logString" }, (field * 100).toInt() ) invalidate() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index e83cded4e2ce..75117936c090 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -22,7 +22,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.os.SystemClock -import android.os.Trace import android.util.IndentingPrintWriter import android.util.Log import android.util.MathUtils @@ -33,6 +32,7 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.Dumpable import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton @@ -263,7 +263,7 @@ constructor( updateScheduled = false val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() val opaque = shouldBlurBeOpaque - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) + TrackTracer.instantForGroup("shade", "shade_blur_radius", blur) blurUtils.applyBlur(root.viewRootImpl, blur, opaque) onBlurApplied(blur, zoomOutFromShadeRadius) } @@ -384,7 +384,7 @@ constructor( windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius -> if (updateScheduled) { // Process the blur applied event only if we scheduled the update - Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", appliedBlurRadius) + TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius) updateScheduled = false onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index a7ad46296e08..ead8f6a1123e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -37,6 +37,7 @@ import android.view.animation.Interpolator; import androidx.annotation.NonNull; import com.android.app.animation.Interpolators; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.compose.animation.scene.OverlayKey; import com.android.compose.animation.scene.SceneKey; import com.android.internal.annotations.GuardedBy; @@ -671,7 +672,7 @@ public class StatusBarStateControllerImpl implements } private void recordHistoricalState(int newState, int lastState, boolean upcoming) { - Trace.traceCounter(Trace.TRACE_TAG_APP, "statusBarState", newState); + TrackTracer.instantForGroup("statusBar", "state", newState); mHistoryIndex = (mHistoryIndex + 1) % HISTORY_SIZE; HistoricalState state = mHistoricalRecords[mHistoryIndex]; state.mNewState = newState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt index 90212ed5b5f7..034a4fd2af72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt @@ -36,7 +36,7 @@ class DataStoreCoordinator internal constructor(private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl) : CoreCoordinator { override fun attach(pipeline: NotifPipeline) { - pipeline.addOnAfterRenderListListener { entries, _ -> onAfterRenderList(entries) } + pipeline.addOnAfterRenderListListener { entries -> onAfterRenderList(entries) } } override fun dumpPipeline(d: PipelineDumper) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index d4d3cdf42fb1..1cb2366a16fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -23,8 +23,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT @@ -51,8 +50,7 @@ internal constructor( groupExpansionManagerImpl.attach(pipeline) } - // TODO: b/293167744 - Remove controller param. - private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = + private fun onAfterRenderList(entries: List<ListEntry>) = traceSection("StackCoordinator.onAfterRenderList") { val notifStats = calculateNotifStats(entries) activeNotificationsInteractor.setNotifStats(notifStats) @@ -84,7 +82,6 @@ internal constructor( } } return NotifStats( - numActiveNotifs = entries.size, hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs, hasClearableAlertingNotifs = hasClearableAlertingNotifs, hasNonClearableSilentNotifs = hasNonClearableSilentNotifs, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java index a34d033afcaa..c58b3febe54b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java @@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder; import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.RenderStageManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory; @@ -89,8 +88,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { public void initialize( NotificationListener notificationService, NotificationRowBinderImpl rowBinder, - NotificationListContainer listContainer, - NotifStackController stackController) { + NotificationListContainer listContainer) { mDumpManager.registerDumpable("NotifPipeline", this); mNotificationService = notificationService; @@ -102,7 +100,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { mNotifPluggableCoordinators.attach(mPipelineWrapper); // Wire up pipeline - mShadeViewManager = mShadeViewManagerFactory.create(listContainer, stackController); + mShadeViewManager = mShadeViewManagerFactory.create(listContainer); mShadeViewManager.attach(mRenderStageManager); mRenderStageManager.attach(mListBuilder); mListBuilder.attach(mNotifCollection); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java index b5a0f7ae169d..ac450c03b850 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import java.util.List; @@ -31,9 +30,6 @@ public interface OnAfterRenderListListener { * * @param entries The current list of top-level entries. Note that this is a live view into the * current list and will change whenever the pipeline is rerun. - * @param controller An object for setting state on the shade. */ - void onAfterRenderList( - @NonNull List<ListEntry> entries, - @NonNull NotifStackController controller); + void onAfterRenderList(@NonNull List<ListEntry> entries); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt deleted file mode 100644 index a37937a6c495..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ /dev/null @@ -1,49 +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.statusbar.notification.collection.render - -import javax.inject.Inject - -/** An interface by which the pipeline can make updates to the notification root view. */ -interface NotifStackController { - /** Provides stats about the list of notifications attached to the shade */ - fun setNotifStats(stats: NotifStats) -} - -/** Data provided to the NotificationRootController whenever the pipeline runs */ -data class NotifStats( - // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag. - val numActiveNotifs: Int, - val hasNonClearableAlertingNotifs: Boolean, - val hasClearableAlertingNotifs: Boolean, - val hasNonClearableSilentNotifs: Boolean, - val hasClearableSilentNotifs: Boolean -) { - companion object { - @JvmStatic val empty = NotifStats(0, false, false, false, false) - } -} - -/** - * An implementation of NotifStackController which provides default, no-op implementations of each - * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods, - * rather than forcing us to add no-op implementations in their implementation every time a method - * is added. - */ -open class DefaultNotifStackController @Inject constructor() : NotifStackController { - override fun setNotifStats(stats: NotifStats) {} -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt index 410b78b9d3bf..8284022c7270 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt @@ -37,12 +37,6 @@ interface NotifViewRenderer { fun onRenderList(notifList: List<ListEntry>) /** - * Provides an interface for the pipeline to update the overall shade. This will be called at - * most once for each time [onRenderList] is called. - */ - fun getStackController(): NotifStackController - - /** * Provides an interface for the pipeline to update individual groups. This will be called at * most once for each group in the most recent call to [onRenderList]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt index 9d3b098fa966..21e68376031c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt @@ -50,7 +50,7 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { traceSection("RenderStageManager.onRenderList") { val viewRenderer = viewRenderer ?: return viewRenderer.onRenderList(notifList) - dispatchOnAfterRenderList(viewRenderer, notifList) + dispatchOnAfterRenderList(notifList) dispatchOnAfterRenderGroups(viewRenderer, notifList) dispatchOnAfterRenderEntries(viewRenderer, notifList) viewRenderer.onDispatchComplete() @@ -85,15 +85,9 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { dump("onAfterRenderEntryListeners", onAfterRenderEntryListeners) } - private fun dispatchOnAfterRenderList( - viewRenderer: NotifViewRenderer, - entries: List<ListEntry>, - ) { + private fun dispatchOnAfterRenderList(entries: List<ListEntry>) { traceSection("RenderStageManager.dispatchOnAfterRenderList") { - val stackController = viewRenderer.getStackController() - onAfterRenderListListeners.forEach { listener -> - listener.onAfterRenderList(entries, stackController) - } + onAfterRenderListListeners.forEach { listener -> listener.onAfterRenderList(entries) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt index 3c838e5b707e..72316bf14c9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt @@ -41,7 +41,6 @@ class ShadeViewManager constructor( @ShadeDisplayAware context: Context, @Assisted listContainer: NotificationListContainer, - @Assisted private val stackController: NotifStackController, mediaContainerController: MediaContainerController, featureManager: NotificationSectionsFeatureManager, sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, @@ -83,8 +82,6 @@ constructor( } } - override fun getStackController(): NotifStackController = stackController - override fun getGroupController(group: GroupEntry): NotifGroupController = viewBarn.requireGroupController(group.requireSummary) @@ -95,8 +92,5 @@ constructor( @AssistedFactory interface ShadeViewManagerFactory { - fun create( - listContainer: NotificationListContainer, - stackController: NotifStackController, - ): ShadeViewManager + fun create(listContainer: NotificationListContainer): ShadeViewManager } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt new file mode 100644 index 000000000000..d7fd7025a94f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data.model + +/** Information about the current list of notifications. */ +data class NotifStats( + val hasNonClearableAlertingNotifs: Boolean, + val hasClearableAlertingNotifs: Boolean, + val hasNonClearableSilentNotifs: Boolean, + val hasClearableSilentNotifs: Boolean, +) { + companion object { + @JvmStatic + val empty = + NotifStats( + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 2b9e49372a63..70f06ebe8468 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 6b93ee1c435e..0c040c855368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 2c5d9c2e449b..3c2051f0b153 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -20,7 +20,6 @@ import android.service.notification.StatusBarNotification import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer /** @@ -33,7 +32,6 @@ interface NotificationsController { fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index ea6a60bd7a1c..0a9899e88d24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -34,7 +34,6 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer @@ -76,7 +75,6 @@ constructor( override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { notificationListener.registerAsSystemService() @@ -101,7 +99,7 @@ constructor( notifPipelineInitializer .get() - .initialize(notificationListener, notificationRowBinder, listContainer, stackController) + .initialize(notificationListener, notificationRowBinder, listContainer) targetSdkResolver.initialize(notifPipeline.get()) notificationsMediaManager.setUpWithPresenter(presenter) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 148b3f021643..92d96f9e899b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -21,7 +21,6 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer import javax.inject.Inject @@ -35,7 +34,6 @@ constructor(private val notificationListener: NotificationListener) : Notificati override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { // Always connect the listener even if notification-handling is disabled. Being a listener diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java index c6832bc20e6d..cc4be57168cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java @@ -20,7 +20,6 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Trace; import android.service.notification.NotificationListenerService; import android.util.ArrayMap; import android.util.ArraySet; @@ -29,6 +28,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; @@ -152,8 +152,8 @@ public class NotificationLogger implements StateListener, CoreStartable, mExpansionStateLogger.onVisibilityChanged( mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", N); - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", + TrackTracer.instantForGroup("Notifications", "Active", N); + TrackTracer.instantForGroup("Notifications", "Visible", mCurrentlyVisibleNotifications.size()); recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 7e3d0043b91a..95604c113a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1267,6 +1267,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); + } else if (android.app.Flags.compactHeadsUpNotification() + && getShowingLayout().isHUNCompact()) { + return getHeadsUpHeight(); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); } else { @@ -3680,6 +3683,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return super.disallowSingleClick(event); } + // TODO: b/388470175 - Although this does get triggered when a notification + // is expanded by the system (e.g. the first notication in the shade), it + // will not be when a notification is collapsed by the system (such as when + // the shade is closed). private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 786d7d9ea0f3..0d2998174121 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -207,6 +207,8 @@ public class NotificationContentView extends FrameLayout implements Notification private boolean mContentAnimating; private UiEventLogger mUiEventLogger; + private boolean mIsHUNCompact; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); @@ -543,6 +545,7 @@ public class NotificationContentView extends FrameLayout implements Notification if (child == null) { mHeadsUpChild = null; mHeadsUpWrapper = null; + mIsHUNCompact = false; if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } @@ -556,8 +559,9 @@ public class NotificationContentView extends FrameLayout implements Notification mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); - if (Flags.compactHeadsUpNotification() - && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) { + mIsHUNCompact = Flags.compactHeadsUpNotification() + && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper; + if (mIsHUNCompact) { logCompactHUNShownEvent(); } @@ -902,6 +906,10 @@ public class NotificationContentView extends FrameLayout implements Notification } } + public boolean isHUNCompact() { + return mIsHUNCompact; + } + private boolean isGroupExpanded() { return mContainingNotification.isGroupExpanded(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index b892bebb3120..c717e3b229be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -35,6 +35,8 @@ import static com.android.systemui.statusbar.notification.stack.StackStateAnimat import android.animation.ObjectAnimator; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.RenderEffect; +import android.graphics.Shader; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; @@ -103,9 +105,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; -import com.android.systemui.statusbar.notification.collection.render.DefaultNotifStackController; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl; @@ -211,9 +211,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationListContainerImpl mNotificationListContainer = new NotificationListContainerImpl(); - // TODO: b/293167744 - Remove this. - private final NotifStackController mNotifStackController = - new DefaultNotifStackController(); @VisibleForTesting final View.OnAttachStateChangeListener mOnAttachStateChangeListener = @@ -1242,6 +1239,22 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Applies a blur effect to the view. + * + * @param blurRadius Radius of blur + */ + public void setBlurRadius(float blurRadius) { + if (blurRadius > 0.0f) { + mView.setRenderEffect(RenderEffect.createBlurEffect( + blurRadius, + blurRadius, + Shader.TileMode.CLAMP)); + } else { + mView.setRenderEffect(null); + } + } + private void updateAlpha() { if (mView != null) { mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard), @@ -1469,10 +1482,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mNotificationListContainer; } - public NotifStackController getNotifStackController() { - return mNotifStackController; - } - public void resetCheckSnoozeLeavebehind() { mView.resetCheckSnoozeLeavebehind(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt index 53749ff24394..c8c798d00a06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -16,9 +16,9 @@ package com.android.systemui.statusbar.notification.stack.ui.view -import android.os.Trace import android.service.notification.NotificationListenerService import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.TrackTracer import com.android.internal.statusbar.IStatusBarService import com.android.internal.statusbar.NotificationVisibility import com.android.systemui.dagger.SysUISingleton @@ -183,8 +183,8 @@ constructor( maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount) updateExpansionStates(newlyVisible, noLongerVisible) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Active]", activeNotifCount) - Trace.traceCounter(Trace.TRACE_TAG_APP, "Notifications [Visible]", newVisibilities.size) + TrackTracer.instantForGroup("Notifications", "Active", activeNotifCount) + TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size) lastLoggedVisibilities.clear() lastLoggedVisibilities.putAll(newVisibilities) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 0b2b84e60f4b..3ea4d488357d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -179,6 +179,10 @@ constructor( } } + if (Flags.bouncerUiRevamp()) { + launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } } + } + if (communalSettingsInteractor.isCommunalFlagEnabled()) { launch { viewModel.glanceableHubAlpha.collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index fc8c70fb8e9a..f0455fc3a22b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -42,6 +42,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel @@ -154,6 +155,7 @@ constructor( private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, + private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, aodBurnInViewModel: AodBurnInViewModel, private val communalSceneInteractor: CommunalSceneInteractor, // Lazy because it's only used in the SceneContainer + Dual Shade configuration. @@ -562,7 +564,7 @@ constructor( lockscreenToDreamingTransitionViewModel.lockscreenAlpha, lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), lockscreenToOccludedTransitionViewModel.lockscreenAlpha, - lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha, alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToGoneTransitionViewModel.notificationAlpha(viewState), @@ -626,6 +628,12 @@ constructor( .dumpWhileCollecting("keyguardAlpha") } + val blurRadius = + primaryBouncerTransitions + .map { transition -> transition.notificationBlurRadius } + .merge() + .dumpWhileCollecting("blurRadius") + /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. 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 3d6cd7e49dfe..b146b92ed110 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1492,7 +1492,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationsController.initialize( mPresenterLazy.get(), mNotifListContainer, - mStackScrollerController.getNotifStackController(), mNotificationActivityStarterLazy.get()); mWindowRootViewVisibilityInteractor.setUp(mPresenterLazy.get(), mNotificationsController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 324db79a4078..d43fed0cbf59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -43,6 +43,7 @@ import android.view.animation.Interpolator; import androidx.annotation.FloatRange; import androidx.annotation.Nullable; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.internal.graphics.ColorUtils; @@ -554,7 +555,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final ScrimState oldState = mState; mState = state; - Trace.traceCounter(Trace.TRACE_TAG_APP, "scrim_state", mState.ordinal()); + TrackTracer.instantForGroup("scrim", "state", mState.ordinal()); if (mCallback != null) { mCallback.onCancelled(); @@ -1279,10 +1280,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump tint = getDebugScrimTint(scrimView); } - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_alpha", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_alpha", (int) (alpha * 255)); - - Trace.traceCounter(Trace.TRACE_TAG_APP, getScrimName(scrimView) + "_tint", + TrackTracer.instantForGroup("scrim", getScrimName(scrimView) + "_tint", Color.alpha(tint)); scrimView.setTint(tint); if (!mIsBouncerToGoneTransitionRunning) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 198859a9013d..8dcb66312558 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.phone; import android.graphics.Color; -import android.os.Trace; +import com.android.app.tracing.coroutines.TrackTracer; import com.android.systemui.dock.DockManager; import com.android.systemui.res.R; import com.android.systemui.scrim.ScrimView; @@ -425,11 +425,11 @@ public enum ScrimState { tint = scrim == mScrimInFront ? ScrimController.DEBUG_FRONT_TINT : ScrimController.DEBUG_BEHIND_TINT; } - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_alpha" : "back_scrim_alpha", (int) (alpha * 255)); - Trace.traceCounter(Trace.TRACE_TAG_APP, + TrackTracer.instantForGroup("scrim", scrim == mScrimInFront ? "front_scrim_tint" : "back_scrim_tint", Color.alpha(tint)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index c31e34c50b06..e622d8f52894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -142,6 +143,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel mLastModifiedVisibility = StatusBarVisibilityModel.createDefaultModel(); private DarkIconManager mDarkIconManager; + private HomeStatusBarViewModel mHomeStatusBarViewModel; + private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory; private final CommandQueue mCommandQueue; private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger; @@ -151,8 +154,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; - private final HomeStatusBarViewModel mHomeStatusBarViewModel; private final HomeStatusBarViewBinder mHomeStatusBarViewBinder; + private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; private final DarkIconManager.Factory mDarkIconManagerFactory; private final SecureSettings mSecureSettings; @@ -256,7 +259,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue ShadeExpansionStateManager shadeExpansionStateManager, StatusBarIconController statusBarIconController, DarkIconManager.Factory darkIconManagerFactory, - HomeStatusBarViewModel homeStatusBarViewModel, + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory, HomeStatusBarViewBinder homeStatusBarViewBinder, StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager, KeyguardStateController keyguardStateController, @@ -281,7 +284,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mAnimationScheduler = animationScheduler; mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarIconController = statusBarIconController; - mHomeStatusBarViewModel = homeStatusBarViewModel; + mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory; mHomeStatusBarViewBinder = homeStatusBarViewBinder; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; mDarkIconManagerFactory = darkIconManagerFactory; @@ -410,6 +413,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCarrierConfigTracker.addCallback(mCarrierConfigCallback); mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener); + mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId); mHomeStatusBarViewBinder.bind( view.getContext().getDisplayId(), mStatusBar, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 96666d83b39b..c71162a22d2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -56,8 +56,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher @@ -148,7 +148,9 @@ abstract class StatusBarPipelineModule { abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable @Binds - abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel + abstract fun homeStatusBarViewModelFactory( + impl: HomeStatusBarViewModelFactoryImpl + ): HomeStatusBarViewModelFactory @Binds abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 0dd7c8499861..2541d84a5a97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -107,10 +107,9 @@ constructor( } if (NotificationsLiveDataStoreRefactor.isEnabled) { - val displayId = view.display.displayId val lightsOutView: View = view.requireViewById(R.id.notification_lights_out) launch { - viewModel.areNotificationsLightsOut(displayId).collect { show -> + viewModel.areNotificationsLightsOut.collect { show -> animateLightsOutView(lightsOutView, show) } } @@ -218,7 +217,7 @@ constructor( StatusBarOperatorNameViewBinder.bind( operatorNameView, viewModel.operatorNameViewModel, - viewModel::areaTint, + viewModel.areaTint, ) launch { viewModel.shouldShowOperatorNameView.collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt index b7744d34560d..5dd76f4434f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt @@ -32,19 +32,16 @@ object StatusBarOperatorNameViewBinder { fun bind( operatorFrameView: View, viewModel: StatusBarOperatorNameViewModel, - areaTint: (Int) -> Flow<StatusBarTintColor>, + areaTint: Flow<StatusBarTintColor>, ) { operatorFrameView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - val displayId = operatorFrameView.display.displayId - val operatorNameText = operatorFrameView.requireViewById<TextView>(R.id.operator_name) launch { viewModel.operatorName.collect { operatorNameText.text = it } } launch { - val tint = areaTint(displayId) - tint.collect { statusBarTintColors -> + areaTint.collect { statusBarTintColors -> operatorNameText.setTextColor( statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen()) ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index f286a1a148fa..b78e010572c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -53,13 +53,14 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIco import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory import javax.inject.Inject /** Factory to simplify the dependency management for [StatusBarRoot] */ class StatusBarRootFactory @Inject constructor( - private val homeStatusBarViewModel: HomeStatusBarViewModel, + private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val homeStatusBarViewBinder: HomeStatusBarViewBinder, private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, private val darkIconManagerFactory: DarkIconManager.Factory, @@ -70,13 +71,14 @@ constructor( ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) + val displayId = root.context.displayId val darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView composeView.apply { setContent { StatusBarRoot( parent = root, - statusBarViewModel = homeStatusBarViewModel, + statusBarViewModel = homeStatusBarViewModelFactory.create(displayId), statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, darkIconManagerFactory = darkIconManagerFactory, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index c9cc17389c17..3f701fc56ab4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -53,7 +52,9 @@ import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteracto import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel -import javax.inject.Inject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -118,6 +119,7 @@ interface HomeStatusBarViewModel { val shouldShowOperatorNameView: Flow<Boolean> val isClockVisible: Flow<VisibilityModel> val isNotificationIconContainerVisible: Flow<VisibilityModel> + /** * Pair of (system info visibility, event animation state). The animation state can be used to * respond to the system event chip animations. In all cases, system info visibility correctly @@ -137,13 +139,13 @@ interface HomeStatusBarViewModel { * whether there are notifications when the device is in * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE]. */ - fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> + val areNotificationsLightsOut: Flow<Boolean> /** - * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will - * allow a view to calculate its correct tint depending on location + * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate + * its correct tint depending on location */ - fun areaTint(displayId: Int): Flow<StatusBarTintColor> + val areaTint: Flow<StatusBarTintColor> /** Models the current visibility for a specific child view of status bar. */ data class VisibilityModel( @@ -157,17 +159,22 @@ interface HomeStatusBarViewModel { val baseVisibility: VisibilityModel, val animationState: SystemEventAnimationState, ) + + /** Interface for the assisted factory, to allow for providing a fake in tests */ + interface HomeStatusBarViewModelFactory { + fun create(displayId: Int): HomeStatusBarViewModel + } } -@SysUISingleton class HomeStatusBarViewModelImpl -@Inject +@AssistedInject constructor( + @Assisted thisDisplayId: Int, homeStatusBarInteractor: HomeStatusBarInteractor, homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor, - private val lightsOutInteractor: LightsOutInteractor, - private val notificationsInteractor: ActiveNotificationsInteractor, - private val darkIconInteractor: DarkIconInteractor, + lightsOutInteractor: LightsOutInteractor, + notificationsInteractor: ActiveNotificationsInteractor, + darkIconInteractor: DarkIconInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, keyguardInteractor: KeyguardInteractor, @@ -211,22 +218,22 @@ constructor( } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = + override val areNotificationsLightsOut: Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { emptyFlow() } else { combine( notificationsInteractor.areAnyNotificationsPresent, - lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false), + lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false), ) { hasNotifications, isLowProfile -> hasNotifications && isLowProfile } .distinctUntilChanged() } - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = darkIconInteractor - .darkState(displayId) + .darkState(thisDisplayId) .map { (areas: Collection<Rect>, tint: Int) -> StatusBarTintColor { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { @@ -364,6 +371,13 @@ constructor( // Similar to the above, but uses INVISIBLE in place of GONE @View.Visibility private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE + + /** Inject this to create the display-dependent view model */ + @AssistedFactory + interface HomeStatusBarViewModelFactoryImpl : + HomeStatusBarViewModel.HomeStatusBarViewModelFactory { + override fun create(displayId: Int): HomeStatusBarViewModelImpl + } } /** Lookup the color for a given view in the status bar */ diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 1da2491c68b6..46d7d5f680ce 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -24,11 +24,15 @@ import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.ViewTreeObserver.InternalInsetsInfo +import android.view.WindowInsets import androidx.constraintlayout.motion.widget.MotionLayout +import androidx.core.view.updatePadding import com.android.internal.view.RotationPolicy +import com.android.systemui.common.ui.view.onApplyWindowInsets import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.util.children +import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.volume.SystemUIInterpolators import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder @@ -43,6 +47,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach @@ -71,6 +76,8 @@ constructor( resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong() fun CoroutineScope.bind(dialog: Dialog) { + val insets: MutableStateFlow<WindowInsets> = + MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) root.alpha = 0f @@ -88,6 +95,22 @@ constructor( launch { root.viewTreeObserver.computeInternalInsetsListener(root) } + launch { + root + .onApplyWindowInsets { v, newInsets -> + val insetsValues = newInsets.getInsets(WindowInsets.Type.displayCutout()) + v.updatePadding( + left = insetsValues.left, + top = insetsValues.top, + right = insetsValues.right, + bottom = insetsValues.bottom, + ) + insets.value = newInsets + WindowInsets.CONSUMED + } + .awaitCancellationThenDispose() + } + with(volumeDialogRingerViewBinder) { bind(root) } with(slidersViewBinder) { bind(root) } with(settingsButtonViewBinder) { bind(root) } @@ -103,8 +126,10 @@ constructor( when (it) { is VolumeDialogVisibilityModel.Visible -> { tracer.traceVisibilityEnd(it) - calculateTranslationX(view)?.let(view::setTranslationX) - view.animateShow(dialogShowAnimationDurationMs) + view.animateShow( + duration = dialogShowAnimationDurationMs, + translationX = calculateTranslationX(view), + ) } is VolumeDialogVisibilityModel.Dismissed -> { tracer.traceVisibilityEnd(it) @@ -134,24 +159,15 @@ constructor( } } - private suspend fun View.animateShow(duration: Long) { + private suspend fun View.animateShow(duration: Long, translationX: Float?) { + translationX?.let { setTranslationX(translationX) } + alpha = 0f animate() .alpha(1f) .translationX(0f) .setDuration(duration) .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator()) .suspendAnimate(jankListenerFactory.show(this, duration)) - /* TODO(b/369993851) - .withEndAction(Runnable { - if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) { - if (mRingerIcon != null) { - mRingerIcon.postOnAnimationDelayed( - getSinglePressFor(mRingerIcon), 1500 - ) - } - } - }) - */ } private suspend fun View.animateHide(duration: Long, translationX: Float?) { @@ -160,22 +176,7 @@ constructor( .alpha(0f) .setDuration(duration) .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator()) - /* TODO(b/369993851) - .withEndAction( - Runnable { - mHandler.postDelayed( - Runnable { - hideRingerDrawer() - - }, - 50 - ) - } - ) - */ - if (translationX != null) { - animator.translationX(translationX) - } + translationX?.let { animator.translationX(it) } animator.suspendAnimate(jankListenerFactory.dismiss(this, duration)) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 4abbbacd800b..047b78eeb04e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -28,9 +28,11 @@ import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INA import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING @@ -159,14 +161,12 @@ class ClockEventControllerTest : SysuiTestCase() { dndModeId = MANUAL_DND_INACTIVE.id zenModeRepository.addMode(MANUAL_DND_INACTIVE) - repository = FakeKeyguardRepository() + repository = kosmos.fakeKeyguardRepository - val withDeps = KeyguardInteractorFactory.create(repository = repository) - - withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) } + kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false) underTest = ClockEventController( - withDeps.keyguardInteractor, + kosmos.keyguardInteractor, keyguardTransitionInteractor, broadcastDispatcher, batteryController, @@ -177,7 +177,7 @@ class ClockEventControllerTest : SysuiTestCase() { mainExecutor, bgExecutor, clockBuffers, - withDeps.featureFlags, + kosmos.fakeFeatureFlagsClassic, zenModeController, kosmos.zenModeInteractor, userTracker, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt index a3c518128b47..f31d49094ac4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -48,7 +47,6 @@ class DataStoreCoordinatorTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() @Before @@ -63,7 +61,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withOneEntry() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf(entry))) verifyNoMoreInteractions(notifLiveDataStoreImpl) } @@ -86,8 +84,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { .setSection(section) .build(), notificationEntry("baz", 1), - ), - stackController, + ) ) val list: List<NotificationEntry> = withArgCaptor { verify(notifLiveDataStoreImpl).setActiveNotifList(capture()) @@ -111,7 +108,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withZeroEntries_whenNewPipelineEnabled() { - afterRenderListListener.onAfterRenderList(listOf(), stackController) + afterRenderListListener.onAfterRenderList(listOf()) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf())) verifyNoMoreInteractions(notifLiveDataStoreImpl) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 77bac59b9dcd..97e99b95f80e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -28,8 +28,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow @@ -43,7 +42,6 @@ import org.junit.runner.RunWith import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @SmallTest @@ -61,7 +59,6 @@ class StackCoordinatorTest : SysuiTestCase() { private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() private val row: ExpandableNotificationRow = mock() @@ -87,25 +84,23 @@ class StackCoordinatorTest : SysuiTestCase() { @Test fun testSetRenderedListOnInteractor() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(renderListInteractor).setRenderedList(eq(listOf(entry))) } @Test fun testSetNotificationStats_clearableAlerting() { whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test @@ -113,35 +108,31 @@ class StackCoordinatorTest : SysuiTestCase() { fun testSetNotificationStats_isSensitiveStateActive_nonClearableAlerting() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test fun testSetNotificationStats_clearableSilent() { whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = true, ) ) - verifyNoMoreInteractions(stackController) } @Test @@ -149,35 +140,31 @@ class StackCoordinatorTest : SysuiTestCase() { fun testSetNotificationStats_isSensitiveStateActive_nonClearableSilent() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test fun testSetNotificationStats_nonClearableRedacted() { entry.setSensitive(true, true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 3a99328fa8ed..30ab416b1cbd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -42,6 +42,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -77,6 +78,8 @@ import com.android.systemui.statusbar.phone.ui.DarkIconManager; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -1268,6 +1271,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mock(StatusBarOperatorNameViewModel.class)); mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder(); + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory = + new HomeStatusBarViewModelFactory() { + @NonNull + @Override + public HomeStatusBarViewModel create(int displayId) { + return mCollapsedStatusBarViewModel; + } + }; + return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, @@ -1275,7 +1287,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mShadeExpansionStateManager, mStatusBarIconController, mIconManagerFactory, - mCollapsedStatusBarViewModel, + homeStatusBarViewModelFactory, mCollapsedStatusBarViewBinder, mStatusBarHideIconsForBouncerManager, mKeyguardStateController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt index 3de809308702..ee21bdc0b4c2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt @@ -24,8 +24,6 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.util.mockito.mock @@ -55,7 +53,6 @@ object KeyguardInteractorFactory { fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(), fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(), fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(), - powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor, testScope: CoroutineScope = TestScope(), ): WithDependencies { // Mock these until they are replaced by kosmos @@ -73,10 +70,8 @@ object KeyguardInteractorFactory { bouncerRepository = bouncerRepository, configurationRepository = configurationRepository, shadeRepository = shadeRepository, - powerInteractor = powerInteractor, KeyguardInteractor( repository = repository, - powerInteractor = powerInteractor, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractorImpl(configurationRepository), shadeRepository = shadeRepository, @@ -99,7 +94,6 @@ object KeyguardInteractorFactory { val bouncerRepository: FakeKeyguardBouncerRepository, val configurationRepository: FakeConfigurationRepository, val shadeRepository: FakeShadeRepository, - val powerInteractor: PowerInteractor, val keyguardInteractor: KeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt index f5f8ef75065f..869bae236d5c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.data.repository.shadeRepository @@ -29,7 +28,6 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by Kosmos.Fixture { KeyguardInteractor( repository = keyguardRepository, - powerInteractor = powerInteractor, bouncerRepository = keyguardBouncerRepository, configurationInteractor = configurationInteractor, shadeRepository = shadeRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt index 15d00d9f6994..edc1cce326c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt @@ -20,4 +20,5 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeBouncerTransition : PrimaryBouncerTransition { override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) + override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index d1619b7959f2..60e092c9709b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @@ -99,6 +100,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, + primaryBouncerTransitions = fakeBouncerTransitions, aodBurnInViewModel = aodBurnInViewModel, communalSceneInteractor = communalSceneInteractor, headsUpNotificationInteractor = { headsUpNotificationInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index b38a723f1fa7..5db0d5a25d83 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel +import android.content.testableContext import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos @@ -36,6 +37,7 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by Kosmos.Fixture { HomeStatusBarViewModelImpl( + testableContext.displayId, homeStatusBarInteractor, homeStatusBarIconBlockListInteractor, lightsOutInteractor, diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto index 648990588d29..3a38152825c9 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -420,5 +420,9 @@ message SystemMessage { // Notify the user that accessibility floating menu is hidden. // Package: com.android.systemui NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009; + + // Notify the hearing aid user that input device can be changed to builtin device or hearing device. + // Package: android + NOTE_HEARING_DEVICE_INPUT_SWITCH = 1012; } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 37d045bf6422..8e037c3ba90c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -413,6 +413,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private SparseArray<SurfaceControl> mA11yOverlayLayers = new SparseArray<>(); private final FlashNotificationsController mFlashNotificationsController; + private final HearingDevicePhoneCallNotificationController mHearingDeviceNotificationController; private final UserManagerInternal mUmi; private AccessibilityUserState getCurrentUserStateLocked() { @@ -541,7 +542,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub MagnificationController magnificationController, @Nullable AccessibilityInputFilter inputFilter, ProxyManager proxyManager, - PermissionEnforcer permissionEnforcer) { + PermissionEnforcer permissionEnforcer, + HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) { super(permissionEnforcer); mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -569,6 +571,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // TODO(b/255426725): not used on tests mVisibleBgUserIds = null; mInputManager = context.getSystemService(InputManager.class); + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mHearingDeviceNotificationController = hearingDeviceNotificationController; + } else { + mHearingDeviceNotificationController = null; + } init(); } @@ -618,6 +625,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } else { mVisibleBgUserIds = null; } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController( + context); + } else { + mHearingDeviceNotificationController = null; + } init(); } @@ -630,6 +643,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (enableTalkbackAndMagnifierKeyGestures()) { mInputManager.registerKeyGestureEventHandler(mKeyGestureEventHandler); } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + if (mHearingDeviceNotificationController != null) { + mHearingDeviceNotificationController.startListenForCallState(); + } + } disableAccessibilityMenuToMigrateIfNeeded(); } diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java index e3d7062ddb4e..b94fa2f59162 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java @@ -22,6 +22,7 @@ import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDIC import android.accessibilityservice.AccessibilityTrace; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -69,7 +70,7 @@ public class AutoclickController extends BaseEventStreamTransformation { // Lazily created on the first mouse motion event. private ClickScheduler mClickScheduler; - private ClickDelayObserver mClickDelayObserver; + private AutoclickSettingsObserver mAutoclickSettingsObserver; private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; private AutoclickIndicatorView mAutoclickIndicatorView; private WindowManager mWindowManager; @@ -89,14 +90,17 @@ public class AutoclickController extends BaseEventStreamTransformation { if (event.isFromSource(InputDevice.SOURCE_MOUSE)) { if (mClickScheduler == null) { Handler handler = new Handler(mContext.getMainLooper()); - mClickScheduler = - new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); - mClickDelayObserver = new ClickDelayObserver(mUserId, handler); - mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler); - if (Flags.enableAutoclickIndicator()) { initiateAutoclickIndicator(handler); } + + mClickScheduler = + new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT); + mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler); + mAutoclickSettingsObserver.start( + mContext.getContentResolver(), + mClickScheduler, + mAutoclickIndicatorScheduler); } handleMouseMotion(event, policyFlags); @@ -156,9 +160,9 @@ public class AutoclickController extends BaseEventStreamTransformation { @Override public void onDestroy() { - if (mClickDelayObserver != null) { - mClickDelayObserver.stop(); - mClickDelayObserver = null; + if (mAutoclickSettingsObserver != null) { + mAutoclickSettingsObserver.stop(); + mAutoclickSettingsObserver = null; } if (mClickScheduler != null) { mClickScheduler.cancel(); @@ -191,19 +195,24 @@ public class AutoclickController extends BaseEventStreamTransformation { } /** - * Observes setting value for autoclick delay, and updates ClickScheduler delay whenever the - * setting value changes. + * Observes autoclick setting values, and updates ClickScheduler delay and indicator size + * whenever the setting value changes. */ - final private static class ClickDelayObserver extends ContentObserver { + final private static class AutoclickSettingsObserver extends ContentObserver { /** URI used to identify the autoclick delay setting with content resolver. */ private final Uri mAutoclickDelaySettingUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY); + /** URI used to identify the autoclick cursor area size setting with content resolver. */ + private final Uri mAutoclickCursorAreaSizeSettingUri = + Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE); + private ContentResolver mContentResolver; private ClickScheduler mClickScheduler; + private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler; private final int mUserId; - public ClickDelayObserver(int userId, Handler handler) { + public AutoclickSettingsObserver(int userId, Handler handler) { super(handler); mUserId = userId; } @@ -216,11 +225,13 @@ public class AutoclickController extends BaseEventStreamTransformation { * changes. * @param clickScheduler ClickScheduler that should be updated when click delay changes. * @throws IllegalStateException If internal state is already setup when the method is - * called. + * called. * @throws NullPointerException If any of the arguments is a null pointer. */ - public void start(@NonNull ContentResolver contentResolver, - @NonNull ClickScheduler clickScheduler) { + public void start( + @NonNull ContentResolver contentResolver, + @NonNull ClickScheduler clickScheduler, + @Nullable AutoclickIndicatorScheduler autoclickIndicatorScheduler) { if (mContentResolver != null || mClickScheduler != null) { throw new IllegalStateException("Observer already started."); } @@ -233,11 +244,20 @@ public class AutoclickController extends BaseEventStreamTransformation { mContentResolver = contentResolver; mClickScheduler = clickScheduler; + mAutoclickIndicatorScheduler = autoclickIndicatorScheduler; mContentResolver.registerContentObserver(mAutoclickDelaySettingUri, false, this, mUserId); // Initialize mClickScheduler's initial delay value. onChange(true, mAutoclickDelaySettingUri); + + if (Flags.enableAutoclickIndicator()) { + // Register observer to listen to cursor area size setting change. + mContentResolver.registerContentObserver( + mAutoclickCursorAreaSizeSettingUri, false, this, mUserId); + // Initialize mAutoclickIndicatorView's initial size. + onChange(true, mAutoclickCursorAreaSizeSettingUri); + } } /** @@ -248,7 +268,7 @@ public class AutoclickController extends BaseEventStreamTransformation { */ public void stop() { if (mContentResolver == null || mClickScheduler == null) { - throw new IllegalStateException("ClickDelayObserver not started."); + throw new IllegalStateException("AutoclickSettingsObserver not started."); } mContentResolver.unregisterContentObserver(this); @@ -262,6 +282,18 @@ public class AutoclickController extends BaseEventStreamTransformation { AccessibilityManager.AUTOCLICK_DELAY_DEFAULT, mUserId); mClickScheduler.updateDelay(delay); } + if (Flags.enableAutoclickIndicator() + && mAutoclickCursorAreaSizeSettingUri.equals(uri)) { + int size = + Settings.Secure.getIntForUser( + mContentResolver, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE, + AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT, + mUserId); + if (mAutoclickIndicatorScheduler != null) { + mAutoclickIndicatorScheduler.updateCursorAreaSize(size); + } + } } } @@ -317,6 +349,10 @@ public class AutoclickController extends BaseEventStreamTransformation { mScheduledShowIndicatorTime = -1; mHandler.removeCallbacks(this); } + + public void updateCursorAreaSize(int size) { + mAutoclickIndicatorView.setRadius(size); + } } /** diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java index 816d8e456a9a..bf5015176f8c 100644 --- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java +++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; +import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; + import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; @@ -35,8 +37,7 @@ public class AutoclickIndicatorView extends View { static final int MINIMAL_ANIMATION_DURATION = 50; - // TODO(b/383901288): allow users to customize the indicator area. - static final float RADIUS = 50; + private float mRadius = AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT; private final Paint mPaint; @@ -84,10 +85,10 @@ public class AutoclickIndicatorView extends View { if (showIndicator) { mRingRect.set( - /* left= */ mX - RADIUS, - /* top= */ mY - RADIUS, - /* right= */ mX + RADIUS, - /* bottom= */ mY + RADIUS); + /* left= */ mX - mRadius, + /* top= */ mY - mRadius, + /* right= */ mX + mRadius, + /* bottom= */ mY + mRadius); canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint); } } @@ -107,6 +108,10 @@ public class AutoclickIndicatorView extends View { mY = y; } + public void setRadius(int radius) { + mRadius = radius; + } + public void redrawIndicator() { showIndicator = true; invalidate(); diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java new file mode 100644 index 000000000000..d06daf5db127 --- /dev/null +++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothUuid; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.media.MediaRecorder; +import android.os.Bundle; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; +import com.android.internal.messages.nano.SystemMessageProto; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * A controller class to handle notification for hearing device during phone calls. + */ +public class HearingDevicePhoneCallNotificationController { + + private final TelephonyManager mTelephonyManager; + private final TelephonyCallback mTelephonyListener; + private final Executor mCallbackExecutor; + + public HearingDevicePhoneCallNotificationController(@NonNull Context context) { + mTelephonyListener = new CallStateListener(context); + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallbackExecutor = Executors.newSingleThreadExecutor(); + } + + @VisibleForTesting + HearingDevicePhoneCallNotificationController(@NonNull Context context, + TelephonyCallback telephonyCallback) { + mTelephonyListener = telephonyCallback; + mTelephonyManager = context.getSystemService(TelephonyManager.class); + mCallbackExecutor = context.getMainExecutor(); + } + + /** + * Registers a telephony callback to listen for call state changed to handle notification for + * hearing device during phone calls. + */ + public void startListenForCallState() { + mTelephonyManager.registerTelephonyCallback(mCallbackExecutor, mTelephonyListener); + } + + /** + * A telephony callback listener to listen to call state changes and show/dismiss notification + */ + @VisibleForTesting + static class CallStateListener extends TelephonyCallback implements + TelephonyCallback.CallStateListener { + + private static final String TAG = + "HearingDevice_CallStateListener"; + private static final String ACTION_SWITCH_TO_BUILTIN_MIC = + "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_BUILTIN_MIC"; + private static final String ACTION_SWITCH_TO_HEARING_MIC = + "com.android.server.accessibility.hearingdevice.action.SWITCH_TO_HEARING_MIC"; + private static final String ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"; + private static final String KEY_BLUETOOTH_ADDRESS = "device_address"; + private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; + private static final int MICROPHONE_SOURCE_VOICE_COMMUNICATION = + MediaRecorder.AudioSource.VOICE_COMMUNICATION; + private static final AudioDeviceAttributes BUILTIN_MIC = new AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, ""); + + private final Context mContext; + private NotificationManager mNotificationManager; + private AudioManager mAudioManager; + private BroadcastReceiver mHearingDeviceActionReceiver; + private BluetoothDevice mHearingDevice; + private boolean mIsNotificationShown = false; + + CallStateListener(@NonNull Context context) { + mContext = context; + } + + @Override + @SuppressLint("AndroidFrameworkRequiresPermission") + public void onCallStateChanged(int state) { + // NotificationManagerService and AudioService are all initialized after + // AccessibilityManagerService. + // Can not get them in constructor. Need to get these services until callback is + // triggered. + mNotificationManager = mContext.getSystemService(NotificationManager.class); + mAudioManager = mContext.getSystemService(AudioManager.class); + if (mNotificationManager == null || mAudioManager == null) { + Log.w(TAG, "NotificationManager or AudioManager is not prepare yet."); + return; + } + + if (state == TelephonyManager.CALL_STATE_IDLE) { + dismissNotificationIfNeeded(); + + if (mHearingDevice != null) { + // reset to its original status + setMicrophonePreferredForCalls(mHearingDevice.isMicrophonePreferredForCalls()); + } + mHearingDevice = null; + } + if (state == TelephonyManager.CALL_STATE_OFFHOOK) { + mHearingDevice = getSupportedInputHearingDeviceInfo( + mAudioManager.getAvailableCommunicationDevices()); + if (mHearingDevice != null) { + showNotificationIfNeeded(); + } + } + } + + private void showNotificationIfNeeded() { + if (mIsNotificationShown) { + return; + } + + showNotification(mHearingDevice.isMicrophonePreferredForCalls()); + mIsNotificationShown = true; + } + + private void dismissNotificationIfNeeded() { + if (!mIsNotificationShown) { + return; + } + + dismissNotification(); + mIsNotificationShown = false; + } + + private void showNotification(boolean useRemoteMicrophone) { + mNotificationManager.notify( + SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH, + createSwitchInputNotification(useRemoteMicrophone)); + registerReceiverIfNeeded(); + } + + private void dismissNotification() { + unregisterReceiverIfNeeded(); + mNotificationManager.cancel( + SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH); + } + + private BluetoothDevice getSupportedInputHearingDeviceInfo(List<AudioDeviceInfo> infoList) { + final BluetoothAdapter bluetoothAdapter = mContext.getSystemService( + BluetoothManager.class).getAdapter(); + if (bluetoothAdapter == null) { + return null; + } + if (!isHapClientSupported()) { + return null; + } + + final Set<String> inputDeviceAddress = Arrays.stream( + mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).map( + AudioDeviceInfo::getAddress).collect(Collectors.toSet()); + + //TODO: b/370812132 - Need to update if TYPE_LEA_HEARING_AID is added + final AudioDeviceInfo hearingDeviceInfo = infoList.stream() + .filter(info -> info.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) + .filter(info -> inputDeviceAddress.contains(info.getAddress())) + .filter(info -> isHapClientDevice(bluetoothAdapter, info)) + .findAny() + .orElse(null); + + return (hearingDeviceInfo != null) ? bluetoothAdapter.getRemoteDevice( + hearingDeviceInfo.getAddress()) : null; + } + + @VisibleForTesting + boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) { + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(info.getAddress()); + return ArrayUtils.contains(device.getUuids(), BluetoothUuid.HAS); + } + + @VisibleForTesting + boolean isHapClientSupported() { + return BluetoothAdapter.getDefaultAdapter().getSupportedProfiles().contains( + BluetoothProfile.HAP_CLIENT); + } + + private Notification createSwitchInputNotification(boolean useRemoteMicrophone) { + return new Notification.Builder(mContext, + SystemNotificationChannels.ACCESSIBILITY_HEARING_DEVICE) + .setContentTitle(getSwitchInputTitle(useRemoteMicrophone)) + .setContentText(getSwitchInputMessage(useRemoteMicrophone)) + .setSmallIcon(R.drawable.ic_settings_24dp) + .setColor(mContext.getResources().getColor( + com.android.internal.R.color.system_notification_accent_color)) + .setLocalOnly(true) + .setCategory(Notification.CATEGORY_SYSTEM) + .setContentIntent(createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)) + .setActions(buildSwitchInputAction(useRemoteMicrophone), + buildOpenSettingsAction()) + .build(); + } + + private Notification.Action buildSwitchInputAction(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_switch_button), + createPendingIntent(ACTION_SWITCH_TO_BUILTIN_MIC)).build() + : new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_switch_button), + createPendingIntent(ACTION_SWITCH_TO_HEARING_MIC)).build(); + } + + private Notification.Action buildOpenSettingsAction() { + return new Notification.Action.Builder(null, + mContext.getString(R.string.hearing_device_notification_settings_button), + createPendingIntent(ACTION_BLUETOOTH_DEVICE_DETAILS)).build(); + } + + private PendingIntent createPendingIntent(String action) { + final Intent intent = new Intent(action); + + switch (action) { + case ACTION_SWITCH_TO_BUILTIN_MIC, ACTION_SWITCH_TO_HEARING_MIC -> { + intent.setPackage(mContext.getPackageName()); + return PendingIntent.getBroadcast(mContext, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + case ACTION_BLUETOOTH_DEVICE_DETAILS -> { + Bundle bundle = new Bundle(); + bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress()); + intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return PendingIntent.getActivity(mContext, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + } + return null; + } + + private void setMicrophonePreferredForCalls(boolean useRemoteMicrophone) { + if (useRemoteMicrophone) { + switchToHearingMic(); + } else { + switchToBuiltinMic(); + } + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + private void switchToBuiltinMic() { + mAudioManager.clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + mAudioManager.setPreferredDeviceForCapturePreset(MICROPHONE_SOURCE_VOICE_COMMUNICATION, + BUILTIN_MIC); + } + + @SuppressLint("AndroidFrameworkRequiresPermission") + private void switchToHearingMic() { + // clear config to let audio manager to determine next priority device. We can assume + // user connects to hearing device here, so next priority device should be hearing + // device. + mAudioManager.clearPreferredDevicesForCapturePreset( + MICROPHONE_SOURCE_VOICE_COMMUNICATION); + } + + private void registerReceiverIfNeeded() { + if (mHearingDeviceActionReceiver != null) { + return; + } + mHearingDeviceActionReceiver = new HearingDeviceActionReceiver(); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_SWITCH_TO_BUILTIN_MIC); + intentFilter.addAction(ACTION_SWITCH_TO_HEARING_MIC); + mContext.registerReceiver(mHearingDeviceActionReceiver, intentFilter, + Manifest.permission.MANAGE_ACCESSIBILITY, null, Context.RECEIVER_NOT_EXPORTED); + } + + private void unregisterReceiverIfNeeded() { + if (mHearingDeviceActionReceiver == null) { + return; + } + mContext.unregisterReceiver(mHearingDeviceActionReceiver); + mHearingDeviceActionReceiver = null; + } + + private CharSequence getSwitchInputTitle(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? mContext.getString( + R.string.hearing_device_switch_phone_mic_notification_title) + : mContext.getString( + R.string.hearing_device_switch_hearing_mic_notification_title); + } + + private CharSequence getSwitchInputMessage(boolean useRemoteMicrophone) { + return useRemoteMicrophone + ? mContext.getString( + R.string.hearing_device_switch_phone_mic_notification_text) + : mContext.getString( + R.string.hearing_device_switch_hearing_mic_notification_text); + } + + private class HearingDeviceActionReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (TextUtils.isEmpty(action)) { + return; + } + + if (ACTION_SWITCH_TO_BUILTIN_MIC.equals(action)) { + switchToBuiltinMic(); + showNotification(/* useRemoteMicrophone= */ false); + } else if (ACTION_SWITCH_TO_HEARING_MIC.equals(action)) { + switchToHearingMic(); + showNotification(/* useRemoteMicrophone= */ true); + } + } + } + } +} diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 57d33f1a051e..43764442e2cf 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -50,7 +50,10 @@ import android.app.appsearch.observer.ObserverSpec; import android.app.appsearch.observer.SchemaChangeInfo; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; +import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -292,7 +295,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { safeExecuteAppFunctionCallback, /* bindFlags= */ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, - callerBinder); + callerBinder, + callingUid); }) .exceptionally( ex -> { @@ -444,7 +448,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { @NonNull ICancellationSignal cancellationSignalTransport, @NonNull SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, int bindFlags, - @NonNull IBinder callerBinder) { + @NonNull IBinder callerBinder, + int callingUid) { CancellationSignal cancellationSignal = CancellationSignal.fromTransport(cancellationSignalTransport); ICancellationCallback cancellationCallback = @@ -465,7 +470,11 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { new RunAppFunctionServiceCallback( requestInternal, cancellationCallback, - safeExecuteAppFunctionCallback), + safeExecuteAppFunctionCallback, + getPackageSigningInfo( + targetUser, + requestInternal.getCallingPackage(), + callingUid)), callerBinder); if (!bindServiceResult) { @@ -477,6 +486,23 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { } } + @NonNull + private SigningInfo getPackageSigningInfo( + @NonNull UserHandle targetUser, @NonNull String packageName, int uid) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(targetUser); + + PackageInfo packageInfo; + packageInfo = + Objects.requireNonNull( + mPackageManagerInternal.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES, + uid, + targetUser.getIdentifier())); + return Objects.requireNonNull(packageInfo.signingInfo); + } + private AppSearchManager getAppSearchManagerAsUser(@NonNull UserHandle userHandle) { return mContext.createContextAsUser(userHandle, /* flags= */ 0) .getSystemService(AppSearchManager.class); diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java index 4cba8ecb2092..0cec09dcde8b 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java +++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java @@ -23,6 +23,7 @@ import android.app.appfunctions.IAppFunctionService; import android.app.appfunctions.ICancellationCallback; import android.app.appfunctions.IExecuteAppFunctionCallback; import android.app.appfunctions.SafeOneTimeExecuteAppFunctionCallback; +import android.content.pm.SigningInfo; import android.os.SystemClock; import android.util.Slog; @@ -38,14 +39,17 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp private final ExecuteAppFunctionAidlRequest mRequestInternal; private final SafeOneTimeExecuteAppFunctionCallback mSafeExecuteAppFunctionCallback; private final ICancellationCallback mCancellationCallback; + private final SigningInfo mCallerSigningInfo; public RunAppFunctionServiceCallback( ExecuteAppFunctionAidlRequest requestInternal, ICancellationCallback cancellationCallback, - SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) { - this.mRequestInternal = requestInternal; - this.mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback; - this.mCancellationCallback = cancellationCallback; + SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback, + SigningInfo callerSigningInfo) { + mRequestInternal = requestInternal; + mSafeExecuteAppFunctionCallback = safeExecuteAppFunctionCallback; + mCancellationCallback = cancellationCallback; + mCallerSigningInfo = callerSigningInfo; } @Override @@ -58,6 +62,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp service.executeAppFunction( mRequestInternal.getClientRequest(), mRequestInternal.getCallingPackage(), + mCallerSigningInfo, mCancellationCallback, new IExecuteAppFunctionCallback.Stub() { @Override diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index d3e808fbd3d1..7456c5099698 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -265,8 +265,8 @@ class InputController { mInputManagerInternal.setPointerIconVisible(visible, displayId); } - void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { - mInputManagerInternal.setMousePointerAccelerationEnabled(enabled, displayId); + void setMouseScalingEnabled(boolean enabled, int displayId) { + mInputManagerInternal.setMouseScalingEnabled(enabled, displayId); } void setDisplayEligibilityForPointerCapture(boolean isEligible, int displayId) { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 6bf60bf1ddf1..260ea75a1f4c 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -1518,7 +1518,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub final long token = Binder.clearCallingIdentity(); try { - mInputController.setMousePointerAccelerationEnabled(false, displayId); + mInputController.setMouseScalingEnabled(false, displayId); mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false, displayId); if (isTrustedDisplay) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b536dc524a80..5a198a106fc5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19374,7 +19374,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) { // this flag will be ramped to public. - intent.collectExtraIntentKeys(); + intent.collectExtraIntentKeys(true); } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index bd2714211796..3f6484f0f58e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -8571,6 +8571,12 @@ public class AudioService extends IAudioService.Stub return true; } + private boolean shouldPreserveVolume(boolean userSwitch, VolumeGroupState vgs) { + // as for STREAM_MUSIC, preserve volume from one user to the next except + // Android Automotive platform + return (userSwitch && vgs.isMusic()) && !isPlatformAutomotive(); + } + private void readVolumeGroupsSettings(boolean userSwitch) { synchronized (mSettingsLock) { synchronized (VolumeStreamState.class) { @@ -8579,8 +8585,7 @@ public class AudioService extends IAudioService.Stub } for (int i = 0; i < sVolumeGroupStates.size(); i++) { VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - // as for STREAM_MUSIC, preserve volume from one user to the next. - if (!(userSwitch && vgs.isMusic())) { + if (!shouldPreserveVolume(userSwitch, vgs)) { vgs.clearIndexCache(); vgs.readSettings(); } @@ -9019,6 +9024,11 @@ public class AudioService extends IAudioService.Stub mIndexMap.clear(); } + private @UserIdInt int getVolumePersistenceUserId() { + return isMusic() && !isPlatformAutomotive() + ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT; + } + private void persistVolumeGroup(int device) { // No need to persist the index if the volume group is backed up // by a public stream type as this is redundant @@ -9036,7 +9046,7 @@ public class AudioService extends IAudioService.Stub boolean success = mSettings.putSystemIntForUser(mContentResolver, getSettingNameForDevice(device), getIndex(device), - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (!success) { Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name()); } @@ -9059,7 +9069,7 @@ public class AudioService extends IAudioService.Stub String name = getSettingNameForDevice(device); index = mSettings.getSystemIntForUser( mContentResolver, name, defaultIndex, - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (index == -1) { continue; } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index c8192e534f5c..b530da2a5f5e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2246,10 +2246,6 @@ public final class DisplayManagerService extends SystemService { @GuardedBy("mSyncRoot") private void handleLogicalDisplayDisconnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayDisconnected shouldn't be received when the flag is off"); - return; - } releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED); mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(display); } @@ -2315,11 +2311,6 @@ public final class DisplayManagerService extends SystemService { @SuppressLint("AndroidFrameworkRequiresPermission") private void handleLogicalDisplayConnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayConnected shouldn't be received when the flag is off"); - return; - } - setupLogicalDisplay(display); if (ExternalDisplayPolicy.isExternalDisplayLocked(display)) { @@ -2346,9 +2337,6 @@ public final class DisplayManagerService extends SystemService { private void handleLogicalDisplayAddedLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); final boolean isDefault = displayId == Display.DEFAULT_DISPLAY; - if (!mFlags.isConnectedDisplayManagementEnabled()) { - setupLogicalDisplay(display); - } // Wake up waitForDefaultDisplay. if (isDefault) { @@ -2443,21 +2431,17 @@ public final class DisplayManagerService extends SystemService { } private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) { - // With display management, the display is removed when disabled, and it might still exist. + // The display is removed when disabled, and it might still exist. // Resources must only be released when the disconnected signal is received. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (display.isValidLocked()) { - updateViewportPowerStateLocked(display); - } + if (display.isValidLocked()) { + updateViewportPowerStateLocked(display); + } - // Note: This method is only called if the display was enabled before being removed. - sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + // Note: This method is only called if the display was enabled before being removed. + sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); - if (display.isValidLocked()) { - applyDisplayChangedLocked(display); - } - } else { - releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + if (display.isValidLocked()) { + applyDisplayChangedLocked(display); } if (mDisplayTopologyCoordinator != null) { mDisplayTopologyCoordinator.onDisplayRemoved(display.getDisplayIdLocked()); @@ -4565,13 +4549,11 @@ public final class DisplayManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if ((internalEventFlagsMask - & DisplayManagerGlobal - .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { - mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, - "Permission required to get signals about connection events."); - } + if ((internalEventFlagsMask + & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, + "Permission required to get signals about connection events."); } final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java index e46397bc8ab7..f6b2591ea440 100644 --- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java +++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java @@ -179,12 +179,10 @@ class DisplayManagerShellCommand extends ShellCommand { pw.println(" Sets brightness to docked + idle screen brightness mode"); pw.println(" undock"); pw.println(" Sets brightness to active (normal) screen brightness mode"); - if (mFlags.isConnectedDisplayManagementEnabled()) { - pw.println(" enable-display DISPLAY_ID"); - pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); - pw.println(" disable-display DISPLAY_ID"); - pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); - } + pw.println(" enable-display DISPLAY_ID"); + pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); + pw.println(" disable-display DISPLAY_ID"); + pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); pw.println(" power-reset DISPLAY_ID"); pw.println(" Turn the DISPLAY_ID power to a state the display supposed to have."); pw.println(" power-off DISPLAY_ID"); @@ -601,11 +599,6 @@ class DisplayManagerShellCommand extends ShellCommand { } private int setDisplayEnabled(boolean enable) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - getErrPrintWriter() - .println("Error: external display management is not available on this device."); - return 1; - } final String displayIdText = getNextArg(); if (displayIdText == null) { getErrPrintWriter().println("Error: no displayId specified"); diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index 519763a1c3db..a47853c8e555 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -142,14 +142,6 @@ class ExternalDisplayPolicy { mDisplayIdsWaitingForBootCompletion.clear(); } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "External display management is not enabled on your device:" - + " cannot register thermal listener."); - } - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "ConnectedDisplayErrorHandlingEnabled is not enabled on your device:" @@ -173,14 +165,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "setExternalDisplayEnabledLocked: External display management is not" - + " enabled on your device, cannot enable/disable display."); - } - return; - } - if (enabled && !isExternalDisplayAllowed()) { Slog.w(TAG, "setExternalDisplayEnabledLocked: External display can not be enabled" + " because it is currently not allowed."); @@ -202,14 +186,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "handleExternalDisplayConnectedLocked connected display management" - + " flag is off"); - } - return; - } - if (!mIsBootCompleted) { mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked()); return; @@ -251,10 +227,6 @@ class ExternalDisplayPolicy { void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) { // Type of the display here is always UNKNOWN, so we can't verify it is an external display - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - var displayId = logicalDisplay.getDisplayIdLocked(); if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) { return; @@ -271,10 +243,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - mExternalDisplayStatsService.onDisplayAdded(logicalDisplay.getDisplayIdLocked()); } @@ -289,10 +257,6 @@ class ExternalDisplayPolicy { } } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - if (isShown) { mExternalDisplayStatsService.onPresentationWindowAdded(displayId); } else { @@ -306,12 +270,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "disableExternalDisplayLocked shouldn't be called when the" - + " connected display management flag is off"); - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "disableExternalDisplayLocked shouldn't be called when the" diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 006921572977..ecc8896b69c6 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -823,18 +823,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (wasPreviouslyUpdated) { // The display isn't actually removed from our internal data structures until // after the notification is sent; see {@link #sendUpdatesForDisplaysLocked}. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (mDisplaysEnabledCache.get(displayId)) { - // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; - } else { - mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; - } + if (mDisplaysEnabledCache.get(displayId)) { + // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; } else { mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; } } else { // This display never left this class, safe to remove without notification @@ -845,20 +840,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // The display is new. } else if (!wasPreviouslyUpdated) { - if (mFlags.isConnectedDisplayManagementEnabled()) { - // We still need to send LOGICAL_DISPLAY_EVENT_ADDED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; - } else { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_ADDED; - } + // We still need to send LOGICAL_DISPLAY_EVENT_ADDED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; // Underlying displays device has changed to a different one. } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) { logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_SWAPPED; // Something about the display device has changed. - } else if (mFlags.isConnectedDisplayManagementEnabled() - && wasPreviouslyEnabled != isCurrentlyEnabled) { + } else if (wasPreviouslyEnabled != isCurrentlyEnabled) { int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED : LOGICAL_DISPLAY_EVENT_REMOVED; logicalDisplayEventMask |= event; @@ -936,17 +926,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED); @@ -996,23 +982,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { + "display=" + id + " with device=" + uniqueId); } - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { - mDisplaysEnabledCache.put(id, true); - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - mDisplaysEnabledCache.delete(id); - } + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { + mDisplaysEnabledCache.put(id, true); + } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { + mDisplaysEnabledCache.delete(id); } mListener.onLogicalDisplayEventLocked(display, logicalDisplayEvent); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { - mLogicalDisplays.delete(id); - } - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - // We wait until we sent the EVENT_REMOVED event before actually removing the - // display. + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { mLogicalDisplays.delete(id); } } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index addfbf1833b9..4e57d6791ff6 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -42,10 +42,6 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT, Flags::enablePortInDisplayLayout); - private final FlagState mConnectedDisplayManagementFlagState = new FlagState( - Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT, - Flags::enableConnectedDisplayManagement); - private final FlagState mAdaptiveToneImprovements1 = new FlagState( Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1, Flags::enableAdaptiveToneImprovements1); @@ -269,11 +265,6 @@ public class DisplayManagerFlags { return mPortInDisplayLayoutFlagState.isEnabled(); } - /** Returns whether connected display management is enabled or not. */ - public boolean isConnectedDisplayManagementEnabled() { - return mConnectedDisplayManagementFlagState.isEnabled(); - } - /** Returns whether power throttling clamper is enabled on not. */ public boolean isPowerThrottlingClamperEnabled() { return mPowerThrottlingClamperFlagState.isEnabled(); @@ -572,7 +563,6 @@ public class DisplayManagerFlags { pw.println(" " + mAdaptiveToneImprovements2); pw.println(" " + mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState); pw.println(" " + mConnectedDisplayErrorHandlingFlagState); - pw.println(" " + mConnectedDisplayManagementFlagState); pw.println(" " + mDisplayOffloadFlagState); pw.println(" " + mExternalDisplayLimitModeState); pw.println(" " + mDisplayTopology); diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index eccbbb14c4ea..afae07c88f8d 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -29,14 +29,6 @@ flag { } flag { - name: "enable_connected_display_management" - namespace: "display_manager" - description: "Feature flag for Connected Display management" - bug: "280739508" - is_fixed_read_only: true -} - -flag { name: "enable_power_throttling_clamper" namespace: "display_manager" description: "Feature flag for Power Throttling Clamper" diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig index eea5c982c537..4505d0e2d799 100644 --- a/services/core/java/com/android/server/flags/services.aconfig +++ b/services/core/java/com/android/server/flags/services.aconfig @@ -78,3 +78,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "datetime_notifications" + # "location" is used by the Android System Time team for feature flags. + namespace: "location" + description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings" + bug: "283267917" +} diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 9f785ac81398..24296406da00 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -19,6 +19,7 @@ package com.android.server.input; import static android.hardware.input.InputGestureData.createKeyTrigger; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; @@ -240,6 +241,13 @@ final class InputGestureManager { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK)); } + if (enableVoiceAccessKeyGestures()) { + systemShortcuts.add( + createKeyGesture( + KeyEvent.KEYCODE_V, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + } if (enableTaskResizingKeyboardShortcuts()) { systemShortcuts.add(createKeyGesture( KeyEvent.KEYCODE_LEFT_BRACKET, diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index bc44fed21f2d..4e5c720f9f1c 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -104,13 +104,16 @@ public abstract class InputManagerInternal { public abstract PointF getCursorPosition(int displayId); /** - * Enables or disables pointer acceleration for mouse movements. + * Set whether all pointer scaling, including linear scaling based on the + * user's pointer speed setting, should be enabled or disabled for mice. * * Note that this only affects pointer movements from mice (that is, pointing devices which send * relative motions, including trackballs and pointing sticks), not from other pointer devices * such as touchpads and styluses. + * + * Scaling is enabled by default on new displays until it is explicitly disabled. */ - public abstract void setMousePointerAccelerationEnabled(boolean enabled, int displayId); + public abstract void setMouseScalingEnabled(boolean enabled, int displayId); /** * Sets the eligibility of windows on a given display for pointer capture. If a display is diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 559b4ae64e50..b2c35e1f362e 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1382,9 +1382,9 @@ public class InputManagerService extends IInputManager.Stub mNative.setPointerSpeed(speed); } - private void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { + private void setMouseScalingEnabled(boolean enabled, int displayId) { updateAdditionalDisplayInputProperties(displayId, - properties -> properties.mousePointerAccelerationEnabled = enabled); + properties -> properties.mouseScalingEnabled = enabled); } private void setPointerIconVisible(boolean visible, int displayId) { @@ -2232,8 +2232,8 @@ public class InputManagerService extends IInputManager.Stub pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i)); final AdditionalDisplayInputProperties properties = mAdditionalDisplayInputProperties.valueAt(i); - pw.println("mousePointerAccelerationEnabled: " - + properties.mousePointerAccelerationEnabled); + pw.println("mouseScalingEnabled: " + + properties.mouseScalingEnabled); pw.println("pointerIconVisible: " + properties.pointerIconVisible); } } finally { @@ -3575,8 +3575,8 @@ public class InputManagerService extends IInputManager.Stub } @Override - public void setMousePointerAccelerationEnabled(boolean enabled, int displayId) { - InputManagerService.this.setMousePointerAccelerationEnabled(enabled, displayId); + public void setMouseScalingEnabled(boolean enabled, int displayId) { + InputManagerService.this.setMouseScalingEnabled(enabled, displayId); } @Override @@ -3716,15 +3716,15 @@ public class InputManagerService extends IInputManager.Stub private static class AdditionalDisplayInputProperties { static final boolean DEFAULT_POINTER_ICON_VISIBLE = true; - static final boolean DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED = true; + static final boolean DEFAULT_MOUSE_SCALING_ENABLED = true; /** - * Whether to enable mouse pointer acceleration on this display. Note that this only affects + * Whether to enable mouse pointer scaling on this display. Note that this only affects * pointer movements from mice (that is, pointing devices which send relative motions, * including trackballs and pointing sticks), not from other pointer devices such as * touchpads and styluses. */ - public boolean mousePointerAccelerationEnabled; + public boolean mouseScalingEnabled; // Whether the pointer icon should be visible or hidden on this display. public boolean pointerIconVisible; @@ -3734,12 +3734,12 @@ public class InputManagerService extends IInputManager.Stub } public boolean allDefaults() { - return mousePointerAccelerationEnabled == DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED + return mouseScalingEnabled == DEFAULT_MOUSE_SCALING_ENABLED && pointerIconVisible == DEFAULT_POINTER_ICON_VISIBLE; } public void reset() { - mousePointerAccelerationEnabled = DEFAULT_MOUSE_POINTER_ACCELERATION_ENABLED; + mouseScalingEnabled = DEFAULT_MOUSE_SCALING_ENABLED; pointerIconVisible = DEFAULT_POINTER_ICON_VISIBLE; } } @@ -3754,14 +3754,14 @@ public class InputManagerService extends IInputManager.Stub mAdditionalDisplayInputProperties.put(displayId, properties); } final boolean oldPointerIconVisible = properties.pointerIconVisible; - final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled; + final boolean oldMouseScalingEnabled = properties.mouseScalingEnabled; updater.accept(properties); if (oldPointerIconVisible != properties.pointerIconVisible) { mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible); } - if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) { - mNative.setMousePointerAccelerationEnabled(displayId, - properties.mousePointerAccelerationEnabled); + if (oldMouseScalingEnabled != properties.mouseScalingEnabled) { + mNative.setMouseScalingEnabled(displayId, + properties.mouseScalingEnabled); } if (properties.allDefaults()) { mAdditionalDisplayInputProperties.remove(displayId); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index d426e8292f14..4d38c8401e2d 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -130,7 +130,7 @@ interface NativeInputManagerService { void setPointerSpeed(int speed); - void setMousePointerAccelerationEnabled(int displayId, boolean enabled); + void setMouseScalingEnabled(int displayId, boolean enabled); void setMouseReverseVerticalScrollingEnabled(boolean enabled); @@ -421,7 +421,7 @@ interface NativeInputManagerService { public native void setPointerSpeed(int speed); @Override - public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled); + public native void setMouseScalingEnabled(int displayId, boolean enabled); @Override public native void setMouseReverseVerticalScrollingEnabled(boolean enabled); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 87d809b5e850..f5ed4d586131 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -32,7 +32,10 @@ import android.hardware.location.ContextHubTransaction; import android.hardware.location.IContextHubTransactionCallback; import android.os.Binder; import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.RemoteException; +import android.os.WorkSource; import android.util.Log; import android.util.SparseArray; @@ -54,6 +57,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Message used by noteOp when this client receives a message from an endpoint. */ private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery"; + /** The duration of wakelocks acquired during HAL callbacks */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; + + /* + * Internal interface used to invoke client callbacks. + */ + interface CallbackConsumer { + void accept(IContextHubEndpointCallback callback) throws RemoteException; + } + /** The context of the service. */ private final Context mContext; @@ -134,6 +147,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final int mUid; + /** Wakelock held while nanoapp message are in flight to the client */ + private final WakeLock mWakeLock; + /* package */ ContextHubEndpointBroker( Context context, IEndpointCommunication hubInterface, @@ -158,6 +174,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mAppOpsManager = context.getSystemService(AppOpsManager.class); mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this); + + PowerManager powerManager = context.getSystemService(PowerManager.class); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName)); + mWakeLock.setReferenceCounted(true); } @Override @@ -302,6 +323,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + public void onCallbackFinished() { + super.onCallbackFinished_enforcePermission(); + releaseWakeLock(); + } + /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { @@ -357,15 +385,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenRequest( - sessionId, initiator, serviceDescriptor); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e); - cleanupSessionResources(sessionId); - return; - } + boolean success = + invokeCallback( + (consumer) -> + consumer.onSessionOpenRequest( + sessionId, initiator, serviceDescriptor)); + if (!success) { + cleanupSessionResources(sessionId); } } @@ -374,14 +400,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId); return; } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionClosed( - sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback( + (consumer) -> + consumer.onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason))); } /* package */ void onEndpointSessionOpenComplete(int sessionId) { @@ -392,13 +415,8 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenComplete(sessionId); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId)); } /* package */ void onMessageReceived(int sessionId, HubMessage message) { @@ -440,14 +458,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub return; } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onMessageReceived(sessionId, message); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onMessageReceived", e); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); - } + boolean success = + invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); + if (!success) { + sendMessageDeliveryStatus( + sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); } } @@ -520,4 +535,46 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions(); return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions); } + + private void acquireWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mIsRegistered.get()) { + mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); + } + }); + } + + private void releaseWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e(TAG, "Releasing the wakelock fails - ", e); + } + } + }); + } + + /** + * Invokes a callback and acquires a wakelock. + * + * @param consumer The callback invoke + * @return false if the callback threw a RemoteException + */ + private boolean invokeCallback(CallbackConsumer consumer) { + if (mContextHubEndpointCallback != null) { + acquireWakeLock(); + try { + consumer.accept(mContextHubEndpointCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling endpoint callback", e); + releaseWakeLock(); + return false; + } + } + return true; + } } diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING index 43e2afd8827d..dbf9915c6e0c 100644 --- a/services/core/java/com/android/server/media/TEST_MAPPING +++ b/services/core/java/com/android/server/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "MediaRouterServiceTests" diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9567c818fa18..dd9741ce9ca1 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3162,6 +3162,7 @@ public class NotificationManagerService extends SystemService { mAssistants.onBootPhaseAppsCanStart(); mConditionProviders.onBootPhaseAppsCanStart(); mHistoryManager.onBootPhaseAppsCanStart(); + mPreferencesHelper.onBootPhaseAppsCanStart(); migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 93f512bc7e17..0bb3c6a067e3 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -36,10 +36,7 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.Person; -import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutInfo; @@ -48,7 +45,6 @@ import android.media.AudioAttributes; import android.media.AudioSystem; import android.metrics.LogMaker; import android.net.Uri; -import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -1493,23 +1489,14 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(uri, false, false); - } else { - oldVisitGrantableUri(uri, false, false); - } + visitGrantableUri(uri, false, false); }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } else { - oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); } } } finally { @@ -1525,53 +1512,6 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { - if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - - if (mGrantableUris != null && mGrantableUris.contains(uri)) { - return; // already verified this URI - } - - final int sourceUid = getSbn().getUid(); - final long ident = Binder.clearCallingIdentity(); - try { - // This will throw a SecurityException if the caller can't grant. - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); - - if (mGrantableUris == null) { - mGrantableUris = new ArraySet<>(); - } - mGrantableUris.add(uri); - } catch (SecurityException e) { - if (!userOverriddenUri) { - if (isSound) { - mSound = Settings.System.DEFAULT_NOTIFICATION_URI; - Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); - } else { - if (mTargetSdkVersion >= Build.VERSION_CODES.P) { - throw e; - } else { - Log.w(TAG, - "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); - } - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - /** - * Note the presence of a {@link Uri} that should have permission granted to - * whoever will be rendering it. - * <p> - * If the enqueuing app has the ability to grant access, it will be added to - * {@link #mGrantableUris}. Otherwise, this will either log or throw - * {@link SecurityException} depending on target SDK of enqueuing app. - */ private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (mGrantableUris != null && mGrantableUris.contains(uri)) { diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 15377d6b269a..36eabae69b22 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -82,7 +82,6 @@ import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseBooleanArray; @@ -272,6 +271,15 @@ public class PreferencesHelper implements RankingConfig { updateMediaNotificationFilteringEnabled(); } + void onBootPhaseAppsCanStart() { + // IpcDataCaches must be invalidated once data becomes available, as queries will only + // begin to be cached after the first invalidation signal. At this point, we know about all + // notification channels. + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } + } + public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -531,12 +539,14 @@ public class PreferencesHelper implements RankingConfig { private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid, int importance, int priority, int visibility, boolean showBadge, int bubblePreference, long creationTime) { + boolean created = false; final String key = packagePreferencesKey(pkg, uid); PackagePreferences r = (uid == UNKNOWN_UID) ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId)) : mPackagePreferences.get(key); if (r == null) { + created = true; r = new PackagePreferences(); r.pkg = pkg; r.uid = uid; @@ -572,6 +582,9 @@ public class PreferencesHelper implements RankingConfig { mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId)); } } + if (android.app.Flags.nmBinderPerfCacheChannels() && created) { + invalidateNotificationChannelCache(); + } return r; } @@ -664,6 +677,9 @@ public class PreferencesHelper implements RankingConfig { } NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW); p.channels.put(channelId, channel); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } return channel; } @@ -1171,9 +1187,7 @@ public class PreferencesHelper implements RankingConfig { // Verify that the app has permission to read the sound Uri // Only check for new channels, as regular apps can only set sound // before creating. See: {@link NotificationChannel#setSound} - if (Flags.notificationVerifyChannelSoundUri()) { - PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); - } + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); @@ -1208,6 +1222,10 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) { + invalidateNotificationChannelCache(); + } + return needsPolicyFileChange; } @@ -1229,6 +1247,9 @@ public class PreferencesHelper implements RankingConfig { } channel.unlockFields(USER_LOCKED_IMPORTANCE); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @@ -1301,6 +1322,9 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } if (changed) { + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } updateConfig(); } } @@ -1537,6 +1561,10 @@ public class PreferencesHelper implements RankingConfig { if (channelBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) { + invalidateNotificationChannelCache(); + } return deletedChannel; } @@ -1566,6 +1594,9 @@ public class PreferencesHelper implements RankingConfig { } r.channels.remove(channelId); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @Override @@ -1576,13 +1607,18 @@ public class PreferencesHelper implements RankingConfig { if (r == null) { return; } + boolean deleted = false; int N = r.channels.size() - 1; for (int i = N; i >= 0; i--) { String key = r.channels.keyAt(i); if (!DEFAULT_CHANNEL_ID.equals(key)) { r.channels.remove(key); + deleted = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) { + invalidateNotificationChannelCache(); + } } } @@ -1613,6 +1649,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void updateDefaultApps(int userId, ArraySet<String> toRemove, @@ -1642,6 +1681,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, @@ -1757,6 +1799,9 @@ public class PreferencesHelper implements RankingConfig { if (groupBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) { + invalidateNotificationChannelCache(); + } return deletedChannels; } @@ -1902,8 +1947,13 @@ public class PreferencesHelper implements RankingConfig { } } } - if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + if (!deletedChannelIds.isEmpty()) { + if (mCurrentUserHasChannelsBypassingDnd) { + updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return deletedChannelIds; } @@ -2196,6 +2246,11 @@ public class PreferencesHelper implements RankingConfig { PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid); prefs.delegate = new Delegate(delegatePkg, delegateUid, true); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + // If package delegates change, then which packages can get what channel information + // also changes, so we need to clear the cache. + invalidateNotificationChannelCache(); + } } /** @@ -2208,6 +2263,9 @@ public class PreferencesHelper implements RankingConfig { prefs.delegate.mEnabled = false; } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } /** @@ -2811,18 +2869,24 @@ public class PreferencesHelper implements RankingConfig { public void onUserRemoved(int userId) { synchronized (mLock) { + boolean removed = false; int N = mPackagePreferences.size(); for (int i = N - 1; i >= 0; i--) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); if (UserHandle.getUserId(PackagePreferences.uid) == userId) { mPackagePreferences.removeAt(i); + removed = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && removed) { + invalidateNotificationChannelCache(); + } } } protected void onLocaleChanged(Context context, int userId) { synchronized (mLock) { + boolean updated = false; int N = mPackagePreferences.size(); for (int i = 0; i < N; i++) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); @@ -2833,10 +2897,14 @@ public class PreferencesHelper implements RankingConfig { DEFAULT_CHANNEL_ID).setName( context.getResources().getString( R.string.default_notification_channel_label)); + updated = true; } // TODO (b/346396459): Localize all reserved channels } } + if (android.app.Flags.nmBinderPerfCacheChannels() && updated) { + invalidateNotificationChannelCache(); + } } } @@ -2884,7 +2952,7 @@ public class PreferencesHelper implements RankingConfig { channel.getAudioAttributes().getUsage()); if (Settings.System.DEFAULT_NOTIFICATION_URI.equals( restoredUri)) { - Log.w(TAG, + Slog.w(TAG, "Could not restore sound: " + uri + " for channel: " + channel); } @@ -2922,6 +2990,9 @@ public class PreferencesHelper implements RankingConfig { if (updated) { updateConfig(); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return updated; } @@ -2939,6 +3010,9 @@ public class PreferencesHelper implements RankingConfig { p.priority = DEFAULT_PRIORITY; p.visibility = DEFAULT_VISIBILITY; p.showBadge = DEFAULT_SHOW_BADGE; + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } } } @@ -3123,6 +3197,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void migrateNotificationPermissions(List<UserInfo> users) { @@ -3154,6 +3231,12 @@ public class PreferencesHelper implements RankingConfig { mRankingHandler.requestSort(); } + @VisibleForTesting + // Utility method for overriding in tests to confirm that the cache gets cleared. + protected void invalidateNotificationChannelCache() { + NotificationManager.invalidateNotificationChannelCache(); + } + private static String packagePreferencesKey(String pkg, int uid) { return pkg + "|" + uid; } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 2b4d71e85dc0..c1ca9c23aef5 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -172,16 +172,6 @@ flag { } flag { - name: "notification_verify_channel_sound_uri" - namespace: "systemui" - description: "Verify Uri permission for sound when creating a notification channel" - bug: "337775777" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_vibration_in_sound_uri_for_channel" namespace: "systemui" description: "Enables sound uri with vibration source in notification channel" diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 17d7a14d9129..e1b76222072e 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -612,7 +612,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable final PackageSetting staticLibPkgSetting = mPm.getPackageSettingForMutation(sharedLibraryInfo.getPackageName()); if (staticLibPkgSetting == null) { - Slog.wtf(TAG, "Shared lib without setting: " + sharedLibraryInfo); + Slog.w(TAG, "Shared lib without setting: " + sharedLibraryInfo); continue; } for (int u = 0; u < installedUserCount; u++) { diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java index 7a5a14d8d3c2..b32943704dc4 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java +++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java @@ -293,8 +293,8 @@ public class PackageUserStateImpl extends WatchableImpl implements PackageUserSt if (mOverlayPaths == null && mSharedLibraryOverlayPaths == null) { return null; } - final OverlayPaths.Builder newPaths = new OverlayPaths.Builder(); - newPaths.addAll(mOverlayPaths); + final OverlayPaths.Builder newPaths = mOverlayPaths == null + ? new OverlayPaths.Builder() : new OverlayPaths.Builder(mOverlayPaths); if (mSharedLibraryOverlayPaths != null) { for (final OverlayPaths libOverlayPaths : mSharedLibraryOverlayPaths.values()) { newPaths.addAll(libOverlayPaths); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 516213b32354..7f511e1e2aa1 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -85,15 +85,16 @@ import static android.view.contentprotection.flags.Flags.createAccessibilityOver import static com.android.hardware.input.Flags.enableNew25q2Keycodes; import static com.android.hardware.input.Flags.enableTalkbackAndMagnifierKeyGestures; +import static com.android.hardware.input.Flags.enableVoiceAccessKeyGestures; import static com.android.hardware.input.Flags.inputManagerLifecycleSupport; import static com.android.hardware.input.Flags.keyboardA11yShortcutControl; import static com.android.hardware.input.Flags.modifierShortcutDump; import static com.android.hardware.input.Flags.overridePowerKeyBehaviorInFocusedWindow; import static com.android.hardware.input.Flags.useKeyGestureEventHandler; +import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.GestureLauncherService.DOUBLE_POWER_TAP_COUNT_THRESHOLD; import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_UNCOVERED; @@ -502,6 +503,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { private TalkbackShortcutController mTalkbackShortcutController; + private VoiceAccessShortcutController mVoiceAccessShortcutController; + private WindowWakeUpPolicy mWindowWakeUpPolicy; /** @@ -2265,6 +2268,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { return new TalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new VoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return new WindowWakeUpPolicy(mContext); } @@ -2512,6 +2519,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); mTalkbackShortcutController = injector.getTalkbackShortcutController(); + mVoiceAccessShortcutController = injector.getVoiceAccessShortcutController(); mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); @@ -4262,6 +4270,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { .isAccessibilityShortcutAvailable(false); case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK: return enableTalkbackAndMagnifierKeyGestures(); + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + return enableVoiceAccessKeyGestures(); default: return false; } @@ -4492,6 +4502,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } break; + case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS: + if (enableVoiceAccessKeyGestures()) { + if (complete) { + mVoiceAccessShortcutController.toggleVoiceAccess(mCurrentUserId); + } + return true; + } + break; case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION: AppLaunchData data = event.getAppLaunchData(); if (complete && canLaunchApp && data != null diff --git a/services/core/java/com/android/server/policy/TalkbackShortcutController.java b/services/core/java/com/android/server/policy/TalkbackShortcutController.java index 9e16a7d5e83a..efda337527d4 100644 --- a/services/core/java/com/android/server/policy/TalkbackShortcutController.java +++ b/services/core/java/com/android/server/policy/TalkbackShortcutController.java @@ -18,20 +18,15 @@ package com.android.server.policy; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; -import android.accessibilityservice.AccessibilityServiceInfo; import android.content.ComponentName; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; import android.os.UserHandle; import android.provider.Settings; -import android.view.accessibility.AccessibilityManager; import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.VisibleForTesting; -import java.util.List; import java.util.Set; /** @@ -42,7 +37,6 @@ import java.util.Set; class TalkbackShortcutController { private static final String TALKBACK_LABEL = "TalkBack"; private final Context mContext; - private final PackageManager mPackageManager; public enum ShortcutSource { GESTURE, @@ -51,7 +45,6 @@ class TalkbackShortcutController { TalkbackShortcutController(Context context) { mContext = context; - mPackageManager = mContext.getPackageManager(); } /** @@ -63,7 +56,10 @@ class TalkbackShortcutController { boolean toggleTalkback(int userId, ShortcutSource source) { final Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); - ComponentName componentName = getTalkbackComponent(); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, TALKBACK_LABEL); + ; if (componentName == null) { return false; } @@ -83,21 +79,6 @@ class TalkbackShortcutController { return isTalkbackAlreadyEnabled; } - private ComponentName getTalkbackComponent() { - AccessibilityManager accessibilityManager = mContext.getSystemService( - AccessibilityManager.class); - List<AccessibilityServiceInfo> serviceInfos = - accessibilityManager.getInstalledAccessibilityServiceList(); - - for (AccessibilityServiceInfo service : serviceInfos) { - final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; - if (isTalkback(serviceInfo)) { - return new ComponentName(serviceInfo.packageName, serviceInfo.name); - } - } - return null; - } - boolean isTalkBackShortcutGestureEnabled() { return Settings.System.getIntForUser(mContext.getContentResolver(), Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED, @@ -120,9 +101,4 @@ class TalkbackShortcutController { ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE, /* serviceEnabled= */ true); } - - private boolean isTalkback(ServiceInfo info) { - return TALKBACK_LABEL.equals(info.loadLabel(mPackageManager).toString()) - && (info.applicationInfo.isSystemApp() || info.applicationInfo.isUpdatedSystemApp()); - } } diff --git a/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java new file mode 100644 index 000000000000..a37fb1140e06 --- /dev/null +++ b/services/core/java/com/android/server/policy/VoiceAccessShortcutController.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 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.policy; + +import android.content.ComponentName; +import android.content.Context; +import android.util.Slog; + +import com.android.internal.accessibility.util.AccessibilityUtils; + +import androidx.annotation.VisibleForTesting; + +import java.util.Set; + +/** This class controls voice access shortcut related operations such as toggling, querying. */ +class VoiceAccessShortcutController { + private static final String TAG = VoiceAccessShortcutController.class.getSimpleName(); + private static final String VOICE_ACCESS_LABEL = "Voice Access"; + + private final Context mContext; + + VoiceAccessShortcutController(Context context) { + mContext = context; + } + + /** + * A function that toggles voice access service. + * + * @return whether voice access is enabled after being toggled. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + boolean toggleVoiceAccess(int userId) { + final Set<ComponentName> enabledServices = + AccessibilityUtils.getEnabledServicesFromSettings(mContext, userId); + ComponentName componentName = + AccessibilityUtils.getInstalledAccessibilityServiceComponentNameByLabel( + mContext, VOICE_ACCESS_LABEL); + if (componentName == null) { + Slog.e(TAG, "Toggle Voice Access failed due to componentName being null"); + return false; + } + + boolean newState = !enabledServices.contains(componentName); + AccessibilityUtils.setAccessibilityServiceState(mContext, componentName, newState, userId); + + return newState; + } +} diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index 14539d544bf9..50db1e4ac30e 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -84,8 +84,12 @@ class RollbackStore { */ private static List<Rollback> loadRollbacks(File rollbackDataDir) { List<Rollback> rollbacks = new ArrayList<>(); - rollbackDataDir.mkdirs(); - for (File rollbackDir : rollbackDataDir.listFiles()) { + File[] rollbackDirs = rollbackDataDir.listFiles(); + if (rollbackDirs == null) { + Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir); + return rollbacks; + } + for (File rollbackDir : rollbackDirs) { if (rollbackDir.isDirectory()) { try { rollbacks.add(loadRollback(rollbackDir)); diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java index 2088e411f842..383135233049 100644 --- a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java @@ -142,11 +142,8 @@ class AggregatedMobileDataStatsPuller { private final RateLimiter mRateLimiter; AggregatedMobileDataStatsPuller(@NonNull NetworkStatsManager networkStatsManager) { - if (DEBUG) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, - TAG + "-AggregatedMobileDataStatsPullerInit"); - } + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-Init"); } mRateLimiter = new RateLimiter(/* window= */ Duration.ofSeconds(1)); @@ -173,10 +170,16 @@ class AggregatedMobileDataStatsPuller { public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime, long unusedUptime) { - mMobileDataStatsHandler.post( + if (mRateLimiter.tryAcquire()) { + mMobileDataStatsHandler.post( () -> { noteUidProcessStateImpl(uid, state); }); + } else { + synchronized (mLock) { + mUidPreviousState.put(uid, state); + } + } } public int pullDataBytesTransfer(List<StatsEvent> data) { @@ -209,29 +212,27 @@ class AggregatedMobileDataStatsPuller { } private void noteUidProcessStateImpl(int uid, int state) { - if (mRateLimiter.tryAcquire()) { - // noteUidProcessStateImpl can be called back to back several times while - // the updateNetworkStats loops over several stats for multiple uids - // and during the first call in a batch of proc state change event it can - // contain info for uid with unknown previous state yet which can happen due to a few - // reasons: - // - app was just started - // - app was started before the ActivityManagerService - // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN - if (mNetworkStatsManager != null) { - updateNetworkStats(mNetworkStatsManager); - } else { - Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); - } + // noteUidProcessStateImpl can be called back to back several times while + // the updateNetworkStats loops over several stats for multiple uids + // and during the first call in a batch of proc state change event it can + // contain info for uid with unknown previous state yet which can happen due to a few + // reasons: + // - app was just started + // - app was started before the ActivityManagerService + // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN + if (mNetworkStatsManager != null) { + updateNetworkStats(mNetworkStatsManager); + } else { + Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); + } + synchronized (mLock) { + mUidPreviousState.put(uid, state); } - mUidPreviousState.put(uid, state); } private void updateNetworkStats(NetworkStatsManager networkStatsManager) { - if (DEBUG) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); - } + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); } final NetworkStats latestStats = networkStatsManager.getMobileUidStats(); @@ -256,20 +257,25 @@ class AggregatedMobileDataStatsPuller { } private void updateNetworkStatsDelta(NetworkStats delta) { + if (DEBUG && Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStatsDelta"); + } synchronized (mLock) { for (NetworkStats.Entry entry : delta) { - if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { - continue; - } - MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); - if (stats != null) { - stats.addTxBytes(entry.getTxBytes()); - stats.addRxBytes(entry.getRxBytes()); - stats.addTxPackets(entry.getTxPackets()); - stats.addRxPackets(entry.getRxPackets()); + if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { + MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); + if (stats != null) { + stats.addTxBytes(entry.getTxBytes()); + stats.addRxBytes(entry.getRxBytes()); + stats.addTxPackets(entry.getTxPackets()); + stats.addRxPackets(entry.getRxPackets()); + } } } } + if (DEBUG) { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } } @GuardedBy("mLock") @@ -298,18 +304,12 @@ class AggregatedMobileDataStatsPuller { } private static boolean isEmpty(NetworkStats stats) { - long totalRxPackets = 0; - long totalTxPackets = 0; for (NetworkStats.Entry entry : stats) { - if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { - continue; + if (entry.getRxPackets() != 0 || entry.getTxPackets() != 0) { + // at least one non empty entry located + return false; } - totalRxPackets += entry.getRxPackets(); - totalTxPackets += entry.getTxPackets(); - // at least one non empty entry located - break; } - final long totalPackets = totalRxPackets + totalTxPackets; - return totalPackets == 0; + return true; } } diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java index 2049a0288f5a..b651c7ba34c3 100644 --- a/services/core/java/com/android/server/timedetector/ServerFlags.java +++ b/services/core/java/com/android/server/timedetector/ServerFlags.java @@ -72,8 +72,12 @@ public final class ServerFlags { KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, KEY_ENHANCED_METRICS_COLLECTION_ENABLED, + KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED }) - @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.SOURCE) @interface DeviceConfigKey {} @@ -192,6 +196,31 @@ public final class ServerFlags { "enhanced_metrics_collection_enabled"; /** + * The key to control support for time zone notifications under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED = + "time_zone_notifications_supported"; + + /** + * The key for the default value used to determine whether time zone notifications is enabled + * when the user hasn't explicitly set it yet. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT = + "time_zone_notifications_enabled_default"; + + /** + * The key to control support for time zone notifications tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED = + "time_zone_notifications_tracking_supported"; + + /** + * The key to control support for time zone manual change tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED = + "time_zone_manual_change_tracking_supported"; + + /** * The registered listeners and the keys to trigger on. The value is explicitly a HashSet to * ensure O(1) lookup performance when working out whether a listener should trigger. */ diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java index 3579246b660f..0495f54cb154 100644 --- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java @@ -286,7 +286,8 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { // This check is racey, but the whole settings update process is racey. This check prevents // a ConfigurationChangeListener callback triggering due to ContentObserver's still // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary - // for stable behavior during tests. + // for stable behavior during tests. This behavior is copied from + // setAutoDetectionEnabledIfRequired and assumed to be the correct way. if (getAutoDetectionEnabledSetting() != enabled) { Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME, enabled ? 1 : 0); } diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java index fc659c5cb627..c4c86a429dd6 100644 --- a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java +++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java @@ -65,6 +65,10 @@ public final class ConfigurationInternal { private final boolean mUserConfigAllowed; private final boolean mLocationEnabledSetting; private final boolean mGeoDetectionEnabledSetting; + private final boolean mNotificationsSupported; + private final boolean mNotificationsEnabledSetting; + private final boolean mNotificationTrackingSupported; + private final boolean mManualChangeTrackingSupported; private ConfigurationInternal(Builder builder) { mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported; @@ -78,6 +82,10 @@ public final class ConfigurationInternal { mUserConfigAllowed = builder.mUserConfigAllowed; mLocationEnabledSetting = builder.mLocationEnabledSetting; mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting; + mNotificationsSupported = builder.mNotificationsSupported; + mNotificationsEnabledSetting = builder.mNotificationsEnabledSetting; + mNotificationTrackingSupported = builder.mNotificationsTrackingSupported; + mManualChangeTrackingSupported = builder.mManualChangeTrackingSupported; } /** Returns true if the device supports any form of auto time zone detection. */ @@ -104,6 +112,27 @@ public final class ConfigurationInternal { } /** + * Returns true if the device supports time-related notifications. + */ + public boolean areNotificationsSupported() { + return mNotificationsSupported; + } + + /** + * Returns true if the device supports tracking of time-related notifications. + */ + public boolean isNotificationTrackingSupported() { + return areNotificationsSupported() && mNotificationTrackingSupported; + } + + /** + * Returns true if the device supports tracking of time zone manual changes. + */ + public boolean isManualChangeTrackingSupported() { + return mManualChangeTrackingSupported; + } + + /** * Returns {@code true} if location time zone detection should run when auto time zone detection * is enabled on supported devices, even when the user has not enabled the algorithm explicitly * in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()} @@ -223,6 +252,15 @@ public final class ConfigurationInternal { && getGeoDetectionRunInBackgroundEnabledSetting(); } + /** Returns true if time-related notifications can be shown on this device. */ + public boolean getNotificationsEnabledBehavior() { + return areNotificationsSupported() && getNotificationsEnabledSetting(); + } + + private boolean getNotificationsEnabledSetting() { + return mNotificationsEnabledSetting; + } + @NonNull public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) { UserHandle userHandle = UserHandle.of(mUserId); @@ -283,6 +321,14 @@ public final class ConfigurationInternal { } builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability); + final @CapabilityState int configureNotificationsEnabledCapability; + if (areNotificationsSupported()) { + configureNotificationsEnabledCapability = CAPABILITY_POSSESSED; + } else { + configureNotificationsEnabledCapability = CAPABILITY_NOT_SUPPORTED; + } + builder.setConfigureNotificationsEnabledCapability(configureNotificationsEnabledCapability); + return builder.build(); } @@ -291,6 +337,7 @@ public final class ConfigurationInternal { return new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(getAutoDetectionEnabledSetting()) .setGeoDetectionEnabled(getGeoDetectionEnabledSetting()) + .setNotificationsEnabled(getNotificationsEnabledSetting()) .build(); } @@ -307,6 +354,9 @@ public final class ConfigurationInternal { if (newConfiguration.hasIsGeoDetectionEnabled()) { builder.setGeoDetectionEnabledSetting(newConfiguration.isGeoDetectionEnabled()); } + if (newConfiguration.hasIsNotificationsEnabled()) { + builder.setNotificationsEnabledSetting(newConfiguration.areNotificationsEnabled()); + } return builder.build(); } @@ -328,7 +378,11 @@ public final class ConfigurationInternal { && mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting && mLocationEnabledSetting == that.mLocationEnabledSetting - && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting; + && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting + && mNotificationsSupported == that.mNotificationsSupported + && mNotificationsEnabledSetting == that.mNotificationsEnabledSetting + && mNotificationTrackingSupported == that.mNotificationTrackingSupported + && mManualChangeTrackingSupported == that.mManualChangeTrackingSupported; } @Override @@ -336,7 +390,9 @@ public final class ConfigurationInternal { return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported, mGeoDetectionSupported, mTelephonyFallbackSupported, mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled, - mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting); + mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting, + mNotificationsSupported, mNotificationsEnabledSetting, + mNotificationTrackingSupported, mManualChangeTrackingSupported); } @Override @@ -352,6 +408,10 @@ public final class ConfigurationInternal { + ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting + ", mLocationEnabledSetting=" + mLocationEnabledSetting + ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting + + ", mNotificationsSupported=" + mNotificationsSupported + + ", mNotificationsEnabledSetting=" + mNotificationsEnabledSetting + + ", mNotificationTrackingSupported=" + mNotificationTrackingSupported + + ", mManualChangeTrackingSupported=" + mManualChangeTrackingSupported + '}'; } @@ -370,6 +430,10 @@ public final class ConfigurationInternal { private boolean mAutoDetectionEnabledSetting; private boolean mLocationEnabledSetting; private boolean mGeoDetectionEnabledSetting; + private boolean mNotificationsSupported; + private boolean mNotificationsEnabledSetting; + private boolean mNotificationsTrackingSupported; + private boolean mManualChangeTrackingSupported; /** * Creates a new Builder. @@ -390,6 +454,10 @@ public final class ConfigurationInternal { this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting; this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting; this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting; + this.mNotificationsSupported = toCopy.mNotificationsSupported; + this.mNotificationsEnabledSetting = toCopy.mNotificationsEnabledSetting; + this.mNotificationsTrackingSupported = toCopy.mNotificationTrackingSupported; + this.mManualChangeTrackingSupported = toCopy.mManualChangeTrackingSupported; } /** @@ -475,6 +543,38 @@ public final class ConfigurationInternal { return this; } + /** + * Sets the value of the time notification setting for this user. + */ + public Builder setNotificationsEnabledSetting(boolean enabled) { + mNotificationsEnabledSetting = enabled; + return this; + } + + /** + * Sets whether time zone notifications are supported on this device. + */ + public Builder setNotificationsSupported(boolean enabled) { + mNotificationsSupported = enabled; + return this; + } + + /** + * Sets whether time zone notification tracking is supported on this device. + */ + public Builder setNotificationsTrackingSupported(boolean supported) { + mNotificationsTrackingSupported = supported; + return this; + } + + /** + * Sets whether time zone manual change tracking are supported on this device. + */ + public Builder setManualChangeTrackingSupported(boolean supported) { + mManualChangeTrackingSupported = supported; + return this; + } + /** Returns a new {@link ConfigurationInternal}. */ @NonNull public ConfigurationInternal build() { diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java index f1248a3f5f0e..d809fc6b6eea 100644 --- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java @@ -68,7 +68,11 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT, ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE, ServerFlags.KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, - ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED + ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED ); /** @@ -100,11 +104,16 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Nullable private static ServiceConfigAccessor sInstance; - @NonNull private final Context mContext; - @NonNull private final ServerFlags mServerFlags; - @NonNull private final ContentResolver mCr; - @NonNull private final UserManager mUserManager; - @NonNull private final LocationManager mLocationManager; + @NonNull + private final Context mContext; + @NonNull + private final ServerFlags mServerFlags; + @NonNull + private final ContentResolver mCr; + @NonNull + private final UserManager mUserManager; + @NonNull + private final LocationManager mLocationManager; @GuardedBy("this") @NonNull @@ -193,6 +202,9 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { contentResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE_EXPLICIT), true, contentObserver); + contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.TIME_ZONE_NOTIFICATIONS), true, + contentObserver); // Add async callbacks for user scoped location settings being changed. contentResolver.registerContentObserver( @@ -331,6 +343,14 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { setGeoDetectionEnabledSettingIfRequired(userId, geoDetectionEnabledSetting); } } + + if (areNotificationsSupported()) { + if (requestedConfigurationUpdates.hasIsNotificationsEnabled()) { + setNotificationsEnabledSetting( + requestedConfigurationUpdates.areNotificationsEnabled()); + } + setNotificationsEnabledIfRequired(newConfiguration.areNotificationsEnabled()); + } } @Override @@ -348,6 +368,10 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { .setUserConfigAllowed(isUserConfigAllowed(userId)) .setLocationEnabledSetting(getLocationEnabledSetting(userId)) .setGeoDetectionEnabledSetting(getGeoDetectionEnabledSetting(userId)) + .setNotificationsSupported(areNotificationsSupported()) + .setNotificationsEnabledSetting(getNotificationsEnabledSetting()) + .setNotificationsTrackingSupported(isNotificationTrackingSupported()) + .setManualChangeTrackingSupported(isManualChangeTrackingSupported()) .build(); } @@ -421,6 +445,49 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { } } + private boolean areNotificationsSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsSupported)); + } + + private boolean isNotificationTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsTrackingSupported)); + } + + private boolean isManualChangeTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneManualChangeTrackingSupported)); + } + + private boolean getNotificationsEnabledSetting() { + final boolean notificationsEnabledByDefault = areNotificationsEnabledByDefault(); + return Settings.Global.getInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, + (notificationsEnabledByDefault ? 1 : 0) /* defaultValue */) != 0; + } + + private boolean areNotificationsEnabledByDefault() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, true); + } + + private void setNotificationsEnabledSetting(boolean enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + + private void setNotificationsEnabledIfRequired(boolean enabled) { + // This check is racey, but the whole settings update process is racey. This check prevents + // a ConfigurationChangeListener callback triggering due to ContentObserver's still + // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary + // for stable behavior during tests. + if (getNotificationsEnabledSetting() != enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + } + @Override public void addLocationTimeZoneManagerConfigListener( @NonNull StateChangeListener listener) { @@ -441,8 +508,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Override public boolean isGeoTimeZoneDetectionFeatureSupportedInConfig() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enableGeolocationTimeZoneDetection); + return getConfigBoolean(R.bool.config_enableGeolocationTimeZoneDetection); } @Override @@ -660,8 +726,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { private boolean isTelephonyFallbackSupported() { return mServerFlags.getBoolean( ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, - getConfigBoolean( - com.android.internal.R.bool.config_supportTelephonyTimeZoneFallback)); + getConfigBoolean(R.bool.config_supportTelephonyTimeZoneFallback)); } private boolean getConfigBoolean(int providerEnabledConfigId) { diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java new file mode 100644 index 000000000000..e14326cc2d53 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.util.IndentingPrintWriter; + +import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.timezonedetector.TimeZoneDetectorStrategy.Origin; + +import java.util.Objects; + +public interface TimeZoneChangeListener { + + /** Record a time zone change. */ + void process(TimeZoneChangeEvent event); + + /** Dump internal state. */ + void dump(IndentingPrintWriter ipw); + + class TimeZoneChangeEvent { + + private final @ElapsedRealtimeLong long mElapsedRealtimeMillis; + private final @CurrentTimeMillisLong long mUnixEpochTimeMillis; + private final @Origin int mOrigin; + private final @UserIdInt int mUserId; + private final String mOldZoneId; + private final String mNewZoneId; + private final @TimeZoneConfidence int mNewConfidence; + private final String mCause; + + public TimeZoneChangeEvent(@ElapsedRealtimeLong long elapsedRealtimeMillis, + @CurrentTimeMillisLong long unixEpochTimeMillis, + @Origin int origin, @UserIdInt int userId, @NonNull String oldZoneId, + @NonNull String newZoneId, int newConfidence, @NonNull String cause) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + mUnixEpochTimeMillis = unixEpochTimeMillis; + mOrigin = origin; + mUserId = userId; + mOldZoneId = Objects.requireNonNull(oldZoneId); + mNewZoneId = Objects.requireNonNull(newZoneId); + mNewConfidence = newConfidence; + mCause = Objects.requireNonNull(cause); + } + + public @ElapsedRealtimeLong long getElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public @CurrentTimeMillisLong long getUnixEpochTimeMillis() { + return mUnixEpochTimeMillis; + } + + public @Origin int getOrigin() { + return mOrigin; + } + + /** + * The ID of the user that triggered the change. + * + * <p>If automatic time zone is turned on, the user ID returned is the system's user id. + */ + public @UserIdInt int getUserId() { + return mUserId; + } + + public String getOldZoneId() { + return mOldZoneId; + } + + public String getNewZoneId() { + return mNewZoneId; + } + + @Override + public String toString() { + return "TimeZoneChangeEvent{" + + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis + + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis + + ", mOrigin=" + mOrigin + + ", mUserId=" + mUserId + + ", mOldZoneId='" + mOldZoneId + '\'' + + ", mNewZoneId='" + mNewZoneId + '\'' + + ", mNewConfidence=" + mNewConfidence + + ", mCause='" + mCause + '\'' + + '}'; + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index d914544566ff..af02ad88ad6a 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -18,6 +18,7 @@ package com.android.server.timezonedetector; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.ITimeZoneDetectorListener; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -73,6 +74,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } @Override + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public void onStart() { // Obtain / create the shared dependencies. Context context = getContext(); @@ -81,7 +83,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub ServiceConfigAccessor serviceConfigAccessor = ServiceConfigAccessorImpl.getInstance(context); TimeZoneDetectorStrategy timeZoneDetectorStrategy = - TimeZoneDetectorStrategyImpl.create(handler, serviceConfigAccessor); + TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor); DeviceActivityMonitor deviceActivityMonitor = DeviceActivityMonitorImpl.create(context, handler); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index 37e67c921634..8cfbe9daa970 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -15,6 +15,7 @@ */ package com.android.server.timezonedetector; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -24,6 +25,11 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.util.IndentingPrintWriter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * The interface for the class that is responsible for setting the time zone on a device, used by * {@link TimeZoneDetectorService} and {@link TimeZoneDetectorInternal}. @@ -97,6 +103,22 @@ import android.util.IndentingPrintWriter; * @hide */ public interface TimeZoneDetectorStrategy extends Dumpable { + @IntDef({ ORIGIN_UNKNOWN, ORIGIN_MANUAL, ORIGIN_TELEPHONY, ORIGIN_LOCATION }) + @Retention(RetentionPolicy.SOURCE) + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @interface Origin {} + + /** Used when the origin of the time zone value cannot be inferred. */ + @Origin int ORIGIN_UNKNOWN = 0; + + /** Used when a time zone value originated from a user / manual settings. */ + @Origin int ORIGIN_MANUAL = 1; + + /** Used when a time zone value originated from a telephony signal. */ + @Origin int ORIGIN_TELEPHONY = 2; + + /** Used when a time zone value originated from a location signal. */ + @Origin int ORIGIN_LOCATION = 3; /** * Adds a listener that will be triggered when something changes that could affect the result diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index dddb46f80724..19a28ddcdaeb 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -28,6 +28,7 @@ import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.DetectorStatusTypes; import android.app.time.LocationTimeZoneAlgorithmStatus; @@ -39,8 +40,10 @@ import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; +import android.content.Context; import android.os.Handler; import android.os.TimestampedValue; +import android.os.UserHandle; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -72,12 +75,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Returns the device's currently configured time zone. May return an empty string. */ - @NonNull String getDeviceTimeZone(); + @NonNull + String getDeviceTimeZone(); /** * Returns the confidence of the device's current time zone. */ - @TimeZoneConfidence int getDeviceTimeZoneConfidence(); + @TimeZoneConfidence + int getDeviceTimeZoneConfidence(); /** * Sets the device's time zone, associated confidence, and records a debug log entry. @@ -115,7 +120,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for an empty or invalid telephony suggestion. * - * Used to score telephony suggestions where there is no zone. + * <p>Used to score telephony suggestions where there is no zone. */ @VisibleForTesting public static final int TELEPHONY_SCORE_NONE = 0; @@ -123,11 +128,11 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a low quality telephony suggestion. * - * Used to score suggestions where: - * The suggested zone ID is one of several possibilities, and the possibilities have different - * offsets. + * <p>Used to score suggestions where: + * The suggested zone ID is one of several possibilities, + * and the possibilities have different offsets. * - * You would have to be quite desperate to want to use this choice. + * <p>You would have to be quite desperate to want to use this choice. */ @VisibleForTesting public static final int TELEPHONY_SCORE_LOW = 1; @@ -135,7 +140,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a medium quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggested zone ID is one of several possibilities but at least the possibilities have the * same offset. Users would get the correct time but for the wrong reason. i.e. their device may * switch to DST at the wrong time and (for example) their calendar events. @@ -146,7 +151,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a high quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggestion was for one zone ID and the answer was unambiguous and likely correct given * the info available. */ @@ -156,7 +161,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a highest quality telephony suggestion. * - * Used for: + * <p>Used for: * Suggestions that must "win" because they constitute test or emulator zone ID. */ @VisibleForTesting @@ -206,7 +211,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private final ServiceConfigAccessor mServiceConfigAccessor; @GuardedBy("this") - @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); + @NonNull + private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); /** * A snapshot of the current detector status. A local copy is cached because it is relatively @@ -244,8 +250,10 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}. */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public static TimeZoneDetectorStrategyImpl create( - @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) { + @NonNull Context context, @NonNull Handler handler, + @NonNull ServiceConfigAccessor serviceConfigAccessor) { Environment environment = new EnvironmentImpl(handler); return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment); @@ -468,7 +476,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // later disables automatic time zone detection. mLatestManualSuggestion.set(suggestion); - setDeviceTimeZoneIfRequired(timeZoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_MANUAL, userId, cause); return true; } @@ -685,7 +693,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are // reliable. - String zoneId; + String timeZoneId; // Introduce bias towards the device's current zone when there are multiple zone suggested. String deviceTimeZone = mEnvironment.getDeviceTimeZone(); @@ -694,11 +702,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, "Geo tz suggestion contains current device time zone. Applying bias."); } - zoneId = deviceTimeZone; + timeZoneId = deviceTimeZone; } else { - zoneId = zoneIds.get(0); + timeZoneId = zoneIds.get(0); } - setDeviceTimeZoneIfRequired(zoneId, detectionReason); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_LOCATION, UserHandle.USER_SYSTEM, + detectionReason); return true; } @@ -779,8 +788,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time // zone ID. - String zoneId = bestTelephonySuggestion.suggestion.getZoneId(); - if (zoneId == null) { + String timeZoneId = bestTelephonySuggestion.suggestion.getZoneId(); + if (timeZoneId == null) { Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason); @@ -790,11 +799,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat String cause = "Found good suggestion:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason; - setDeviceTimeZoneIfRequired(zoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_TELEPHONY, UserHandle.USER_SYSTEM, cause); } @GuardedBy("this") - private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) { + private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @Origin int origin, + @UserIdInt int userId, @NonNull String cause) { String currentZoneId = mEnvironment.getDeviceTimeZone(); // All manual and automatic suggestions are considered high confidence as low-quality // suggestions are not currently passed on. diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java index 54ae047a2858..0b676ff7d590 100644 --- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java +++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java @@ -100,6 +100,11 @@ final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter { } VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile(); + if (frequencyProfile.isEmpty()) { + // The frequency profile has an invalid frequency range, so keep the segments unchanged. + return repeatIndex; + } + float[] frequenciesHz = frequencyProfile.getFrequenciesHz(); float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs(); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 89c7a3d89a54..6f308aa9b706 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1631,7 +1631,7 @@ class ActivityMetricsLogger { int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; if (isAppCompateStateChangedToLetterboxed(state)) { - positionToLog = activity.mAppCompatController.getAppCompatReachabilityOverrides() + positionToLog = activity.mAppCompatController.getReachabilityOverrides() .getLetterboxPositionForLogging(); } FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 29f1f93a844f..3d53078de3c3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4261,7 +4261,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void finishRelaunching() { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(false); mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this); @@ -8222,7 +8222,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedConfiguration.getMergedConfiguration())) { ensureActivityConfiguration(false /* ignoreVisibility */); if (mPendingRelaunchCount > originalRelaunchingCount) { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(true); } if (mTransitionController.inPlayingTransition(this)) { @@ -8744,7 +8744,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A navBarInsets = Insets.NONE; } final AppCompatReachabilityOverrides reachabilityOverrides = - mAppCompatController.getAppCompatReachabilityOverrides(); + mAppCompatController.getReachabilityOverrides(); // Horizontal position int offsetX = 0; if (parentBounds.width() != screenResolvedBoundsWidth) { @@ -10217,7 +10217,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatAspectRatioOverrides() .shouldOverrideMinAspectRatio()); proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP, - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .shouldIgnoreOrientationRequestLoop()); proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP, mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp()); diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 0967078deac3..6d0e8eacd438 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -33,7 +33,7 @@ class AppCompatController { @NonNull private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy; @NonNull - private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; + private final AppCompatReachabilityPolicy mReachabilityPolicy; @NonNull private final DesktopAppCompatAspectRatioPolicy mDesktopAppCompatAspectRatioPolicy; @NonNull @@ -58,7 +58,7 @@ class AppCompatController { mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides); mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, mTransparentPolicy, mAppCompatOverrides); - mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord, + mReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord, wmService.mAppCompatConfiguration); mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(activityRecord, wmService.mAppCompatConfiguration); @@ -89,8 +89,8 @@ class AppCompatController { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOverrides.getAppCompatOrientationOverrides(); + AppCompatOrientationOverrides getOrientationOverrides() { + return mAppCompatOverrides.getOrientationOverrides(); } @NonNull @@ -109,8 +109,8 @@ class AppCompatController { } @NonNull - AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return mAppCompatReachabilityPolicy; + AppCompatReachabilityPolicy getReachabilityPolicy() { + return mReachabilityPolicy; } @NonNull @@ -124,8 +124,8 @@ class AppCompatController { } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatOverrides.getAppCompatReachabilityOverrides(); + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mAppCompatOverrides.getReachabilityOverrides(); } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index e929fb414340..449458665b63 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -154,7 +154,7 @@ class AppCompatLetterboxPolicy { @VisibleForTesting boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { - if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + if (mActivityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()) { return mLastShouldShowLetterboxUi; } @@ -205,7 +205,7 @@ class AppCompatLetterboxPolicy { } pw.println(prefix + " letterboxReason=" + AppCompatUtils.getLetterboxReasonString(mActivityRecord, mainWin)); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy().dump(pw, prefix); + mActivityRecord.mAppCompatController.getReachabilityPolicy().dump(pw, prefix); final AppCompatLetterboxOverrides letterboxOverride = mActivityRecord.mAppCompatController .getAppCompatLetterboxOverrides(); pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString( @@ -276,12 +276,12 @@ class AppCompatLetterboxPolicy { final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord .mAppCompatController.getAppCompatLetterboxOverrides(); final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord - .mAppCompatController.getAppCompatReachabilityPolicy(); + .mAppCompatController.getReachabilityPolicy(); mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), mActivityRecord.mWmService.mTransactionFactory, reachabilityPolicy, letterboxOverrides, this::getLetterboxParentSurface); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); } final Point letterboxPosition = new Point(); @@ -291,7 +291,7 @@ class AppCompatLetterboxPolicy { final Rect innerFrame = new Rect(); calculateLetterboxInnerBounds(mActivityRecord, w, innerFrame); mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition); - if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides() + if (mActivityRecord.mAppCompatController.getReachabilityOverrides() .isDoubleTapEvent()) { // We need to notify Shell that letterbox position has changed. mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); @@ -321,7 +321,7 @@ class AppCompatLetterboxPolicy { mLetterbox.destroy(); mLetterbox = null; } - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } @@ -415,7 +415,7 @@ class AppCompatLetterboxPolicy { calculateLetterboxPosition(mActivityRecord, mLetterboxPosition); calculateLetterboxOuterBounds(mActivityRecord, mOuterBounds); calculateLetterboxInnerBounds(mActivityRecord, w, mInnerBounds); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(() -> mInnerBounds); } @@ -438,7 +438,7 @@ class AppCompatLetterboxPolicy { mLetterboxPosition.set(0, 0); mInnerBounds.setEmpty(); mOuterBounds.setEmpty(); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index c84711d4be51..af83668f1188 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -113,7 +113,7 @@ class AppCompatOrientationOverrides { // Task to ensure that Activity Embedding is excluded. return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null && mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && mActivityRecord.mAppCompatController.getOrientationOverrides() .isOverrideRespectRequestedOrientationEnabled(); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 16e20297dcf3..fc758ef90995 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -94,7 +94,7 @@ class AppCompatOrientationPolicy { return SCREEN_ORIENTATION_PORTRAIT; } - if (mAppCompatOverrides.getAppCompatOrientationOverrides() + if (mAppCompatOverrides.getOrientationOverrides() .isAllowOrientationOverrideOptOut()) { return candidate; } @@ -108,7 +108,7 @@ class AppCompatOrientationPolicy { } final AppCompatOrientationOverrides.OrientationOverridesState capabilityState = - mAppCompatOverrides.getAppCompatOrientationOverrides() + mAppCompatOverrides.getOrientationOverrides() .mOrientationOverridesState; if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled @@ -170,7 +170,7 @@ class AppCompatOrientationPolicy { boolean shouldIgnoreRequestedOrientation( @ActivityInfo.ScreenOrientation int requestedOrientation) { final AppCompatOrientationOverrides orientationOverrides = - mAppCompatOverrides.getAppCompatOrientationOverrides(); + mAppCompatOverrides.getOrientationOverrides(); if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) { if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { Slog.w(TAG, "Ignoring orientation update to " diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 58b37becc373..9fb54db23d55 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -27,7 +27,7 @@ import com.android.server.wm.utils.OptPropFactory; public class AppCompatOverrides { @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; + private final AppCompatOrientationOverrides mOrientationOverrides; @NonNull private final AppCompatCameraOverrides mAppCompatCameraOverrides; @NonNull @@ -37,7 +37,7 @@ public class AppCompatOverrides { @NonNull private final AppCompatResizeOverrides mResizeOverrides; @NonNull - private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; + private final AppCompatReachabilityOverrides mReachabilityOverrides; @NonNull private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; @@ -48,13 +48,13 @@ public class AppCompatOverrides { @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, + mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides); - mAppCompatReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, + mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, appCompatConfiguration, appCompatDeviceStateQuery); mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord, appCompatConfiguration, optPropBuilder, appCompatDeviceStateQuery, - mAppCompatReachabilityOverrides); + mReachabilityOverrides); mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager, @@ -64,8 +64,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOrientationOverrides; + AppCompatOrientationOverrides getOrientationOverrides() { + return mOrientationOverrides; } @NonNull @@ -89,8 +89,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatReachabilityOverrides; + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mReachabilityOverrides; } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index d03a80387657..087edc184b6f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -77,7 +77,7 @@ class AppCompatReachabilityPolicy { void dump(@NonNull PrintWriter pw, @NonNull String prefix) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); pw.println(prefix + " isVerticalThinLetterboxed=" + reachabilityOverrides .isVerticalThinLetterboxed()); pw.println(prefix + " isHorizontalThinLetterboxed=" + reachabilityOverrides @@ -96,7 +96,7 @@ class AppCompatReachabilityPolicy { private void handleHorizontalDoubleTap(int x) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; @@ -142,7 +142,7 @@ class AppCompatReachabilityPolicy { private void handleVerticalDoubleTap(int y) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 9f88bc952351..e28dddc496e1 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -138,7 +138,7 @@ final class AppCompatUtils { return; } final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController - .getAppCompatReachabilityOverrides(); + .getReachabilityOverrides(); final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED); final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible(); // Whether the direct top activity is in size compat mode. diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index a4e58ef923b8..d6ae65193121 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -108,9 +108,7 @@ final class ContentRecorder implements WindowContainerListener { ContentRecorder(@NonNull DisplayContent displayContent) { this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId), - new DisplayManagerFlags().isConnectedDisplayManagementEnabled() - && !new DisplayManagerFlags() - .isPixelAnisotropyCorrectionInLogicalDisplayEnabled() + !new DisplayManagerFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled() && displayContent.getDisplayInfo().type == Display.TYPE_EXTERNAL); } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index f40d636b522a..b932ef362aca 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -264,7 +264,7 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { // that should be respected, Check all activities in display to make sure any eligible // activity should be respected. final ActivityRecord activity = mDisplayContent.getActivity((r) -> - r.mAppCompatController.getAppCompatOrientationOverrides() + r.mAppCompatController.getOrientationOverrides() .shouldRespectRequestedOrientationDueToOverride()); return activity != null; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 145c7b37fcdc..dd23f577e05b 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2964,7 +2964,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (!handlesOrientationChangeFromDescendant(orientation)) { ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true); if (topActivity != null && topActivity.mAppCompatController - .getAppCompatOrientationOverrides() + .getOrientationOverrides() .shouldUseDisplayLandscapeNaturalOrientation()) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is ignoring orientation request for %d, return %d" diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f0faa8e4691f..a9646783b92d 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1412,6 +1412,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (!tr.isAttached() || !tr.isVisibleRequested() || !tr.inPinnedWindowingMode()) return; final ActivityRecord currTop = tr.getTopNonFinishingActivity(); + if (currTop == null) return; if (currTop.inPinnedWindowingMode()) return; Slog.e(TAG, "Enter-PIP was started but not completed, this is a Shell/SysUI" + " bug. This state breaks gesture-nav, so attempting clean-up."); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 3c6778ecbb30..92e0931993d1 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -10177,9 +10177,10 @@ public class WindowManagerService extends IWindowManager.Stub throw new SecurityException("Access denied to process: " + pid + ", must have permission " + Manifest.permission.ACCESS_FPS_COUNTER); } - - if (mRoot.anyTaskForId(taskId) == null) { - throw new IllegalArgumentException("no task with taskId: " + taskId); + synchronized (mGlobalLock) { + if (mRoot.anyTaskForId(taskId) == null) { + throw new IllegalArgumentException("no task with taskId: " + taskId); + } } mTaskFpsCallbackController.registerListener(taskId, callback); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b43e334d6e1a..3a2a1ea419d4 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5424,7 +5424,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // change then delay the position update until it has redrawn to avoid any flickers. final boolean isLetterboxedAndRelaunching = activityRecord != null && activityRecord.areBoundsLetterboxed() - && activityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && activityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged(); if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) { applyWithNextDraw(mSetSurfacePositionConsumer); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 379e312e58c0..911c686c711f 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -343,7 +343,7 @@ public: void setPointerDisplayId(ui::LogicalDisplayId displayId); int32_t getMousePointerSpeed(); void setPointerSpeed(int32_t speed); - void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled); + void setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled); void setMouseReverseVerticalScrollingEnabled(bool enabled); void setMouseScrollingAccelerationEnabled(bool enabled); void setMouseScrollingSpeed(int32_t speed); @@ -474,8 +474,8 @@ private: // Pointer speed. int32_t pointerSpeed{0}; - // Displays on which its associated mice will have pointer acceleration disabled. - std::set<ui::LogicalDisplayId> displaysWithMousePointerAccelerationDisabled{}; + // Displays on which its associated mice will have all scaling disabled. + std::set<ui::LogicalDisplayId> displaysWithMouseScalingDisabled{}; // True if pointer gestures are enabled. bool pointerGesturesEnabled{true}; @@ -603,9 +603,8 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); - dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n", - dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled, - streamableToString) + dump += StringPrintf(INDENT "Display with Mouse Scaling Disabled: %s\n", + dumpSet(mLocked.displaysWithMouseScalingDisabled, streamableToString) .c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", toString(mLocked.pointerGesturesEnabled)); @@ -834,13 +833,11 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon std::scoped_lock _l(mLock); outConfig->mousePointerSpeed = mLocked.pointerSpeed; - outConfig->displaysWithMousePointerAccelerationDisabled = - mLocked.displaysWithMousePointerAccelerationDisabled; + outConfig->displaysWithMouseScalingDisabled = mLocked.displaysWithMouseScalingDisabled; outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = - mLocked.displaysWithMousePointerAccelerationDisabled.count( - mLocked.pointerDisplayId) == 0 + mLocked.displaysWithMouseScalingDisabled.count(mLocked.pointerDisplayId) == 0 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION : 1; outConfig->wheelVelocityControlParameters.acceleration = @@ -1519,23 +1516,21 @@ void NativeInputManager::setPointerSpeed(int32_t speed) { InputReaderConfiguration::Change::POINTER_SPEED); } -void NativeInputManager::setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, - bool enabled) { +void NativeInputManager::setMouseScalingEnabled(ui::LogicalDisplayId displayId, bool enabled) { { // acquire lock std::scoped_lock _l(mLock); - const bool oldEnabled = - mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0; + const bool oldEnabled = mLocked.displaysWithMouseScalingDisabled.count(displayId) == 0; if (oldEnabled == enabled) { return; } - ALOGI("Setting mouse pointer acceleration to %s on display %s", toString(enabled), + ALOGI("Setting mouse pointer scaling to %s on display %s", toString(enabled), displayId.toString().c_str()); if (enabled) { - mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId); + mLocked.displaysWithMouseScalingDisabled.erase(displayId); } else { - mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId); + mLocked.displaysWithMouseScalingDisabled.emplace(displayId); } } // release lock @@ -2589,11 +2584,11 @@ static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed im->setPointerSpeed(speed); } -static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj, - jint displayId, jboolean enabled) { +static void nativeSetMouseScalingEnabled(JNIEnv* env, jobject nativeImplObj, jint displayId, + jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setMousePointerAccelerationEnabled(ui::LogicalDisplayId{displayId}, enabled); + im->setMouseScalingEnabled(ui::LogicalDisplayId{displayId}, enabled); } static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { @@ -3340,8 +3335,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouchOnDisplay}, {"getMousePointerSpeed", "()I", (void*)nativeGetMousePointerSpeed}, {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, - {"setMousePointerAccelerationEnabled", "(IZ)V", - (void*)nativeSetMousePointerAccelerationEnabled}, + {"setMouseScalingEnabled", "(IZ)V", (void*)nativeSetMouseScalingEnabled}, {"setMouseReverseVerticalScrollingEnabled", "(Z)V", (void*)nativeSetMouseReverseVerticalScrollingEnabled}, {"setMouseScrollingAccelerationEnabled", "(Z)V", diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 2627895b8c63..e69a7414dd76 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -21907,7 +21907,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { accountToMigrate, sourceUser, targetUser, - /* callback= */ null, /* handler= */ null) + /* handler= */ null, /* callback= */ null) .getResult(60 * 3, TimeUnit.SECONDS); if (copySucceeded) { logCopyAccountStatus(COPY_ACCOUNT_SUCCEEDED, callerPackage); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index a9ad435762ad..02e5470e8673 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -415,7 +415,6 @@ public class DisplayManagerServiceTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); mLocalServiceKeeperRule.overrideLocalService( InputManagerInternal.class, mMockInputManagerInternal); @@ -2797,30 +2796,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withoutDisplayManagement_shouldAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - manageDisplaysPermission(/* granted= */ true); - DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); - DisplayManagerService.BinderService bs = displayManager.new BinderService(); - LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); - FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); - callback.expectsEvent(EVENT_DISPLAY_ADDED); - - FakeDisplayDevice displayDevice = - createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL); - callback.waitForExpectedEvent(); - - LogicalDisplay display = - logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true); - assertThat(display.isEnabledLocked()).isTrue(); - assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED); - - } - - @Test - public void testConnectExternalDisplay_withDisplayManagement_shouldDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_shouldDisableDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -2849,9 +2825,8 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagementAndSysprop_shouldEnableDisplay() { + public void testConnectExternalDisplay_withSysprop_shouldEnableDisplay() { Assume.assumeTrue(Build.IS_ENG || Build.IS_USERDEBUG); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); doAnswer((Answer<Boolean>) invocationOnMock -> true) .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); manageDisplaysPermission(/* granted= */ true); @@ -2883,8 +2858,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagement_allowsEnableAndDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_allowsEnableAndDisableDisplay() { when(mMockFlags.isApplyDisplayChangedDuringDisplayAddedEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); LocalServices.addService(WindowManagerPolicy.class, mMockedWindowManagerPolicy); @@ -2955,8 +2929,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectInternalDisplay_shouldConnectAndAddDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); @@ -3011,7 +2984,7 @@ public class DisplayManagerServiceTest { DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); + bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS); callback.expectsEvent(EVENT_DISPLAY_ADDED); FakeDisplayDevice displayDevice = @@ -3032,8 +3005,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldSignalDisplayAdded() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3062,8 +3034,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldThrowException() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3087,8 +3058,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableInternalDisplay_withManageDisplays_shouldSignalAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableInternalDisplay_shouldSignalAdded() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3115,8 +3085,7 @@ public class DisplayManagerServiceTest { } @Test - public void testDisableInternalDisplay_withDisplayManagement_shouldSignalRemove() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisableInternalDisplay_shouldSignalRemove() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3140,7 +3109,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_shouldSignalDisplayRemoved() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3181,7 +3149,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3207,7 +3174,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenDisabled_shouldSignalDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3244,7 +3210,6 @@ public class DisplayManagerServiceTest { @Test public void testRegisterCallback_withoutPermission_shouldThrow() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); @@ -3255,7 +3220,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3288,7 +3252,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveInternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index 782262d3f7c9..a48a88cecbc2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -22,7 +22,6 @@ import static android.view.Display.TYPE_INTERNAL; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -47,7 +46,6 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.testutils.TestHandler; -import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; @@ -124,7 +122,6 @@ public class ExternalDisplayPolicyTest { public void setup() throws Exception { MockitoAnnotations.initMocks(this); mHandler = new TestHandler(/*callback=*/ null); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn(true); when(mMockedInjector.getFlags()).thenReturn(mMockedFlags); when(mMockedInjector.getLogicalDisplayMapper()).thenReturn(mMockedLogicalDisplayMapper); @@ -173,16 +170,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testTryEnableExternalDisplay_featureDisabled(@TestParameter final boolean enable) { - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, enable); - mHandler.flush(); - verify(mMockedLogicalDisplayMapper, never()).setDisplayEnabledLocked(any(), anyBoolean()); - verify(mMockedDisplayNotificationManager, never()) - .onHighTemperatureExternalDisplayNotAllowed(); - } - - @Test public void testTryDisableExternalDisplay_criticalThermalCondition() throws RemoteException { // Disallow external displays due to thermals. setTemperature(registerThermalListener(), List.of(CRITICAL_TEMPERATURE)); @@ -278,21 +265,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testNoThermalListenerRegistered_featureDisabled( - @TestParameter final boolean isConnectedDisplayManagementEnabled, - @TestParameter final boolean isErrorHandlingEnabled) throws RemoteException { - assumeFalse(isConnectedDisplayManagementEnabled && isErrorHandlingEnabled); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn( - isConnectedDisplayManagementEnabled); - when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn( - isErrorHandlingEnabled); - - mExternalDisplayPolicy.onBootCompleted(); - verify(mMockedThermalService, never()).registerThermalEventListenerWithType( - any(), anyInt()); - } - - @Test public void testOnCriticalTemperature_disallowAndAllowExternalDisplay() throws RemoteException { final var thermalListener = registerThermalListener(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 0dbb6ba58b3c..7d3cd8a8a9ae 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -222,7 +222,6 @@ public class LogicalDisplayMapperTest { when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any(), anyBoolean())) .thenAnswer(AdditionalAnswers.returnsSecondArg()); - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false); mLooper = new TestLooper(); mHandler = new Handler(mLooper.getLooper()); mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mFoldSettingProviderMock, @@ -351,8 +350,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDeviceAddAndRemove_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDeviceAddAndRemove() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); @@ -390,8 +388,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDisableEnable_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDisableEnable() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay displayAdded = add(device); @@ -1350,9 +1347,14 @@ public class LogicalDisplayMapperTest { ArgumentCaptor<LogicalDisplay> displayCaptor = ArgumentCaptor.forClass(LogicalDisplay.class); verify(mListenerMock).onLogicalDisplayEventLocked( - displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_CONNECTED)); + LogicalDisplay display = displayCaptor.getValue(); + if (display.isEnabledLocked()) { + verify(mListenerMock).onLogicalDisplayEventLocked( + eq(display), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + } clearInvocations(mListenerMock); - return displayCaptor.getValue(); + return display; } private void testDisplayDeviceAddAndRemove_NonInternal(int type) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index fa78dfce0a17..dafe4827b2fe 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -220,6 +220,9 @@ public class AccessibilityManagerServiceTest { @Mock private ProxyManager mProxyManager; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock + private HearingDevicePhoneCallNotificationController + mMockHearingDevicePhoneCallNotificationController; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -289,7 +292,8 @@ public class AccessibilityManagerServiceTest { mMockMagnificationController, mInputFilter, mProxyManager, - mFakePermissionEnforcer); + mFakePermissionEnforcer, + mMockHearingDevicePhoneCallNotificationController); mA11yms.switchUser(mTestableContext.getUserId()); mTestableLooper.processAllMessages(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java new file mode 100644 index 000000000000..efea21428937 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.accessibility; + +import static android.Manifest.permission.BLUETOOTH_PRIVILEGED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Application; +import android.app.Instrumentation; +import android.app.NotificationManager; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.media.AudioDeviceInfo; +import android.media.AudioDevicePort; +import android.media.AudioManager; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; + +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.messages.nano.SystemMessageProto; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Tests for the {@link HearingDevicePhoneCallNotificationController}. + */ +@RunWith(AndroidJUnit4.class) +public class HearingDevicePhoneCallNotificationControllerTest { + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; + + private final Application mApplication = ApplicationProvider.getApplicationContext(); + @Spy + private final Context mContext = mApplication.getApplicationContext(); + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + + @Mock + private TelephonyManager mTelephonyManager; + @Mock + private NotificationManager mNotificationManager; + @Mock + private AudioManager mAudioManager; + private HearingDevicePhoneCallNotificationController mController; + private TestCallStateListener mTestCallStateListener; + + @Before + public void setUp() { + mInstrumentation.getUiAutomation().adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED); + when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager); + when(mContext.getSystemService(NotificationManager.class)).thenReturn(mNotificationManager); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + + mTestCallStateListener = new TestCallStateListener(mContext); + mController = new HearingDevicePhoneCallNotificationController(mContext, + mTestCallStateListener); + mController.startListenForCallState(); + } + + @Test + public void startListenForCallState_callbackNotNull() { + Mockito.reset(mTelephonyManager); + mController = new HearingDevicePhoneCallNotificationController(mContext); + ArgumentCaptor<TelephonyCallback> listenerCaptor = ArgumentCaptor.forClass( + TelephonyCallback.class); + + mController.startListenForCallState(); + + verify(mTelephonyManager).registerTelephonyCallback(any(Executor.class), + listenerCaptor.capture()); + TelephonyCallback callback = listenerCaptor.getValue(); + assertThat(callback).isNotNull(); + } + + @Test + public void onCallStateChanged_stateOffHook_hapDevice_showNotification() { + AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLE_HEADSET); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + + verify(mNotificationManager).notify( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any()); + } + + @Test + public void onCallStateChanged_stateOffHook_a2dpDevice_noNotification() { + AudioDeviceInfo a2dpDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLUETOOTH_A2DP); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{a2dpDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(a2dpDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + + verify(mNotificationManager, never()).notify( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH), any()); + } + + @Test + public void onCallStateChanged_stateOffHookThenIdle_hapDeviceInfo_cancelNotification() { + AudioDeviceInfo hapDeviceInfo = createAudioDeviceInfo(TEST_ADDRESS, + AudioManager.DEVICE_OUT_BLE_HEADSET); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[]{hapDeviceInfo}); + when(mAudioManager.getAvailableCommunicationDevices()).thenReturn(List.of(hapDeviceInfo)); + + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK); + mTestCallStateListener.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE); + + verify(mNotificationManager).cancel( + eq(SystemMessageProto.SystemMessage.NOTE_HEARING_DEVICE_INPUT_SWITCH)); + } + + private AudioDeviceInfo createAudioDeviceInfo(String address, int type) { + AudioDevicePort audioDevicePort = mock(AudioDevicePort.class); + doReturn(type).when(audioDevicePort).type(); + doReturn(address).when(audioDevicePort).address(); + doReturn("testDevice").when(audioDevicePort).name(); + + return new AudioDeviceInfo(audioDevicePort); + } + + /** + * For easier testing for CallStateListener, override methods that contain final object. + */ + private static class TestCallStateListener extends + HearingDevicePhoneCallNotificationController.CallStateListener { + + TestCallStateListener(@NonNull Context context) { + super(context); + } + + @Override + boolean isHapClientSupported() { + return true; + } + + @Override + boolean isHapClientDevice(BluetoothAdapter bluetoothAdapter, AudioDeviceInfo info) { + return TEST_ADDRESS.equals(info.getAddress()); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 32578a7dc10f..bdbb495db841 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -340,8 +340,7 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - doNothing().when(mInputManagerInternalMock) - .setMousePointerAccelerationEnabled(anyBoolean(), anyInt()); + doNothing().when(mInputManagerInternalMock).setMouseScalingEnabled(anyBoolean(), anyInt()); doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt()); LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 0d86d4c3fa28..60a4b9a499c1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -402,6 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } + /** public void testPushDynamicShortcut() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," @@ -543,6 +544,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10)); } + */ public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() throws InterruptedException { diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java new file mode 100644 index 000000000000..47e3dc85f6d0 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.UserIdInt; + +public final class ConfigInternalForTests { + + static final @UserIdInt int USER_ID = 9876; + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(false) + .setGeoDetectionFeatureSupported(false) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java index fc6afe486187..aeb4d9a19ff0 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java @@ -31,7 +31,7 @@ import java.util.Optional; /** * A partially implemented, fake implementation of ServiceConfigAccessor for tests. * - * <p>This class has rudamentary support for multiple users, but unlike the real thing, it doesn't + * <p>This class has rudimentary support for multiple users, but unlike the real thing, it doesn't * simulate that some settings are global and shared between users. It also delivers config updates * synchronously. */ diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index e52e8b60a61d..47a9b2c47173 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -35,6 +35,12 @@ import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_U import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DETECT_NOT_SUPPORTED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DISABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW; @@ -68,6 +74,7 @@ import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; import android.service.timezone.TimeZoneProviderStatus; +import android.util.IndentingPrintWriter; import com.android.server.SystemTimeZone.TimeZoneConfidence; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion; @@ -82,6 +89,7 @@ import org.junit.runner.RunWith; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -92,7 +100,6 @@ import java.util.function.Function; @RunWith(JUnitParamsRunner.class) public class TimeZoneDetectorStrategyImplTest { - private static final @UserIdInt int USER_ID = 9876; private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; /** A time zone used for initialization that does not occur elsewhere in tests. */ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; @@ -101,7 +108,7 @@ public class TimeZoneDetectorStrategyImplTest { // Telephony test cases are ordered so that each successive one is of the same or higher score // than the previous. - private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[] { + private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[]{ newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW), newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, @@ -118,90 +125,6 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_HIGHEST), }; - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(false) - .setGeoDetectionFeatureSupported(false) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS = new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); @@ -421,7 +344,7 @@ public class TimeZoneDetectorStrategyImplTest { new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion, TELEPHONY_SCORE_NONE); script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null); assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -629,7 +552,7 @@ public class TimeZoneDetectorStrategyImplTest { */ @Test public void testTelephonySuggestionMultipleSlotIndexSuggestionScoringAndSlotIndexBias() { - String[] zoneIds = { "Europe/London", "Europe/Paris" }; + String[] zoneIds = {"Europe/London", "Europe/Paris"}; TelephonyTimeZoneSuggestion emptySlotIndex1Suggestion = createEmptySlotIndex1Suggestion(); TelephonyTimeZoneSuggestion emptySlotIndex2Suggestion = createEmptySlotIndex2Suggestion(); QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex1ScoredSuggestion = @@ -672,7 +595,7 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); assertEquals(expectedZoneSlotIndex1ScoredSuggestion, @@ -805,14 +728,14 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeEnabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -834,7 +757,7 @@ public class TimeZoneDetectorStrategyImplTest { } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeDisabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -849,7 +772,7 @@ public class TimeZoneDetectorStrategyImplTest { ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris"); boolean expectedResult = userConfigAllowed || bypassUserPolicyChecks; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); if (expectedResult) { script.verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); @@ -1258,7 +1181,6 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); - } // Demonstrate what happens when geolocation is uncertain when telephony fallback is @@ -1569,7 +1491,7 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); expectedDeviceTimeZoneId = manualSuggestion.getZoneId(); assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, @@ -1880,6 +1802,7 @@ public class TimeZoneDetectorStrategyImplTest { boolean actualResult = mTimeZoneDetectorStrategy.suggestManualTimeZone( userId, manualTimeZoneSuggestion, bypassUserPolicyChecks); assertEquals(expectedResult, actualResult); + return this; } @@ -2001,4 +1924,34 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTestCase(matchType, quality, expectedScore); } + static class FakeTimeZoneChangeEventListener implements TimeZoneChangeListener { + private final List<TimeZoneChangeEvent> mEvents = new ArrayList<>(); + + FakeTimeZoneChangeEventListener() { + } + + @Override + public void process(TimeZoneChangeEvent event) { + mEvents.add(event); + } + + public List<TimeZoneChangeEvent> getTimeZoneChangeEvents() { + return mEvents; + } + + @Override + public void dump(IndentingPrintWriter ipw) { + // No-op for tests + } + } + + private static void assertEmpty(Collection<?> collection) { + assertTrue( + "Expected empty, but contains (" + collection.size() + ") elements: " + collection, + collection.isEmpty()); + } + + private static void assertNotEmpty(Collection<?> collection) { + assertFalse("Expected not empty: " + collection, collection.isEmpty()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 8e79514c875e..f41805d40b0d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -66,7 +66,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; -import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -164,6 +163,7 @@ import com.android.os.AtomsProto.PackageNotificationChannelPreferences; import com.android.os.AtomsProto.PackageNotificationPreferences; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -179,6 +179,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -199,9 +202,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA) @@ -239,9 +239,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { private NotificationManager.Policy mTestNotificationPolicy; - private PreferencesHelper mHelper; - // fresh object for testing xml reading - private PreferencesHelper mXmlHelper; + private TestPreferencesHelper mHelper; + // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting + // with real IpcDataCaches + private TestPreferencesHelper mXmlHelper; private AudioAttributes mAudioAttributes; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); @@ -378,10 +379,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds); when(mClock.millis()).thenReturn(System.currentTimeMillis()); - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); resetZenModeHelper(); @@ -793,7 +794,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_oldXml_migrates() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -929,7 +930,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -988,7 +989,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_permissionNotificationOff() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); @@ -1047,7 +1048,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -1641,7 +1642,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { serializer.flush(); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); @@ -1696,7 +1697,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { Duration.ofDays(2).toMillis() + System.currentTimeMillis()); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); @@ -1774,10 +1775,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow( new FileNotFoundException("")).thenReturn(resId); - mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); @@ -3190,7 +3191,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); @@ -3210,7 +3210,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); @@ -3229,7 +3228,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); @@ -6573,4 +6571,223 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.setCanBePromoted(PKG_P, UID_P, false, false); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnCreationAndChange() { + mHelper.resetCacheInvalidation(); + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // new channel should invalidate the cache. + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // when the channel data is updated, should invalidate the cache again after that. + mHelper.resetCacheInvalidation(); + NotificationChannel newChannel = channel.copy(); + newChannel.setName("new name"); + newChannel.setImportance(IMPORTANCE_HIGH); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // also for conversations + mHelper.resetCacheInvalidation(); + String parentId = "id"; + String convId = "conversation"; + NotificationChannel conv = new NotificationChannel( + String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation", + IMPORTANCE_DEFAULT); + conv.setConversationId(parentId, convId); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + mHelper.resetCacheInvalidation(); + NotificationChannel newConv = conv.copy(); + newConv.setName("changed"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnDelete() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // recreate channel and now permanently delete + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + mHelper.resetCacheInvalidation(); + mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id"); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_noInvalidationWhenNoChange() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + // newChannel, same as the old channel + NotificationChannel newChannel = channel.copy(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1, + false); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + + // because there were no effective changes, we should not see any cache invalidations + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // deletions of a nonexistent channel also don't change anything + mHelper.resetCacheInvalidation(); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_multipleUsersAndPackages() { + // Setup: create channels for: + // pkg O, user + // pkg O, work (same channel ID, different user) + // pkg N_MR1, user + // pkg N_MR1, user, conversation child of above + String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv"); + NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT); + NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT); + NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT); + NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv", + IMPORTANCE_DEFAULT); + p2u1Conv.setConversationId("p2", "conv"); + + mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true, + false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true, + false, UID_O + UserHandle.PER_USER_RANGE, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true, + false, UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true, + false, UID_N_MR1, false); + mHelper.resetCacheInvalidation(); + + // Update to an existent channel, with a change: should invalidate + NotificationChannel p1u1New = p1u1.copy(); + p1u1New.setName("p1u1 new"); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // Do it again, but no change for this user + mHelper.resetCacheInvalidation(); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Delete conversations, but for a package without those conversations + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Now delete conversations for the right package + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()), + UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_userRemoved() throws Exception { + NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT); + int uid1 = UserHandle.getUid(1, 1); + setUpPackageWithUid("pkg1", uid1); + mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false); + mHelper.resetCacheInvalidation(); + + // delete user 1; should invalidate cache + mHelper.onUserRemoved(1); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_packagesChanged() { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + + // package deleted: expect cache invalidation + mHelper.resetCacheInvalidation(); + mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // re-created: expect cache invalidation again + mHelper.resetCacheInvalidation(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_flagOff_neverTouchesCache() { + // Do a bunch of channel-changing operations. + NotificationChannel channel = + new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + + NotificationChannel copy = channel.copy(); + copy.setName("name2"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + // Test version of PreferencesHelper whose only functional difference is that it does not + // interact with the real IpcDataCache, and instead tracks whether or not the cache has been + // invalidated since creation or the last reset. + private static class TestPreferencesHelper extends PreferencesHelper { + private boolean mCacheInvalidated = false; + + TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, + NotificationChannelLogger notificationChannelLogger, + AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, + boolean showReviewPermissionsNotification, Clock clock) { + super(context, pm, rankingHandler, zenHelper, permHelper, permManager, + notificationChannelLogger, appOpsManager, userProfiles, ugmInternal, + showReviewPermissionsNotification, clock); + } + + @Override + protected void invalidateNotificationChannelCache() { + mCacheInvalidated = true; + } + + boolean hasCacheBeenInvalidated() { + return mCacheInvalidated; + } + + void resetCacheInvalidation() { + mCacheInvalidated = false; + } + } } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java new file mode 100644 index 000000000000..09f573cd1ee0 --- /dev/null +++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vibrator; + +import static com.google.common.truth.Truth.assertThat; + +import android.hardware.vibrator.IVibrator; +import android.os.VibratorInfo; +import android.os.vibrator.BasicPwleSegment; +import android.os.vibrator.Flags; +import android.os.vibrator.PwleSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +public class BasicToPwleSegmentAdapterTest { + + private static final float TEST_RESONANT_FREQUENCY = 150; + private static final float[] TEST_FREQUENCIES = + new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f}; + private static final float[] TEST_OUTPUT_ACCELERATIONS = + new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f}; + + private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES, + TEST_OUTPUT_ACCELERATIONS); + + private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null); + + private BasicToPwleSegmentAdapter mAdapter; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + mAdapter = new BasicToPwleSegmentAdapter(); + } + + @Test + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + VibratorInfo vibratorInfo = createVibratorInfo( + EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100))); + List<VibrationEffectSegment> expectedSegments = Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100)); + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(expectedSegments); + } + + private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile, + int... capabilities) { + return new VibratorInfo.Builder(0) + .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0)) + .setFrequencyProfile(frequencyProfile) + .build(); + } +} diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java index 9d4d94bebfd9..85ef466b2477 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java @@ -758,6 +758,18 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES) + public void testKeyGestureToggleVoiceAccess() { + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(true); + + Assert.assertTrue( + sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS)); + mPhoneWindowManager.assertVoiceAccess(false); + } + + @Test public void testKeyGestureToggleDoNotDisturb() { mPhoneWindowManager.overrideZenMode(Settings.Global.ZEN_MODE_OFF); Assert.assertTrue( diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 6c48ba26a475..4ff3d433632a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -201,6 +201,8 @@ class TestPhoneWindowManager { private boolean mIsTalkBackEnabled; private boolean mIsTalkBackShortcutGestureEnabled; + private boolean mIsVoiceAccessEnabled; + private Intent mBrowserIntent; private Intent mSmsIntent; @@ -225,6 +227,18 @@ class TestPhoneWindowManager { } } + private class TestVoiceAccessShortcutController extends VoiceAccessShortcutController { + TestVoiceAccessShortcutController(Context context) { + super(context); + } + + @Override + boolean toggleVoiceAccess(int currentUserId) { + mIsVoiceAccessEnabled = !mIsVoiceAccessEnabled; + return mIsVoiceAccessEnabled; + } + } + private class TestInjector extends PhoneWindowManager.Injector { TestInjector(Context context, WindowManagerPolicy.WindowManagerFuncs funcs) { super(context, funcs); @@ -260,6 +274,10 @@ class TestPhoneWindowManager { return new TestTalkbackShortcutController(mContext); } + VoiceAccessShortcutController getVoiceAccessShortcutController() { + return new TestVoiceAccessShortcutController(mContext); + } + WindowWakeUpPolicy getWindowWakeUpPolicy() { return mWindowWakeUpPolicy; } @@ -1024,6 +1042,11 @@ class TestPhoneWindowManager { Assert.assertEquals(expectEnabled, mIsTalkBackEnabled); } + void assertVoiceAccess(boolean expectEnabled) { + mTestLooper.dispatchAll(); + Assert.assertEquals(expectEnabled, mIsVoiceAccessEnabled); + } + void assertKeyGestureEventSentToKeyGestureController(int gestureType) { verify(mInputManagerInternal) .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType)); 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 c9cbe0fa08c5..6fad82b26808 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -210,7 +210,7 @@ public class ActivityRecordTests extends WindowTestsBase { } private TestStartingWindowOrganizer registerTestStartingWindowOrganizer() { - return new TestStartingWindowOrganizer(mAtm); + return new TestStartingWindowOrganizer(mAtm, mDisplayContent); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java index 9d191cea8acb..a0727a7af87b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -335,7 +335,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index a21ab5de5de2..4faa71451a4d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -601,7 +601,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java index 463254caa845..50419d46f48f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -159,8 +159,8 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -196,7 +196,7 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java index ddc4de9cfd8a..09b8bce2c930 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java @@ -246,8 +246,8 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -281,12 +281,12 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } @NonNull private AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return activity().top().mAppCompatController.getAppCompatReachabilityPolicy(); + return activity().top().mAppCompatController.getReachabilityPolicy(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index ea925c019b77..4854f0d948b4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -88,7 +88,7 @@ public class DisplayPolicyTests extends WindowTestsBase { } private WindowState createDreamWindow() { - final WindowState win = createDreamWindow(null, TYPE_BASE_APPLICATION, "dream"); + final WindowState win = createDreamWindow("dream", TYPE_BASE_APPLICATION); final WindowManager.LayoutParams attrs = win.mAttrs; attrs.width = MATCH_PARENT; attrs.height = MATCH_PARENT; diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 9d9f24cb50f2..96b11a87d8df 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -330,7 +330,7 @@ public class SizeCompatTests extends WindowTestsBase { if (horizontalReachability) { final Consumer<Integer> doubleClick = (Integer x) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(x, displayHeight / 2); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -360,7 +360,7 @@ public class SizeCompatTests extends WindowTestsBase { } else { final Consumer<Integer> doubleClick = (Integer y) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(displayWidth / 2, y); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -421,7 +421,7 @@ public class SizeCompatTests extends WindowTestsBase { final Consumer<Integer> doubleClick = (Integer y) -> { - activity.mAppCompatController.getAppCompatReachabilityPolicy() + activity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(dw / 2, y); activity.mRootWindowContainer.performSurfacePlacement(); }; @@ -834,7 +834,7 @@ public class SizeCompatTests extends WindowTestsBase { // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); assertTrue(mActivity.isRelaunching()); - assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides() + assertTrue(mActivity.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); @@ -3427,7 +3427,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false); final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled()); assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled()); } @@ -3451,7 +3451,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Horizontal reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3475,7 +3475,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Vertical reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3498,7 +3498,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is disabled because the app does not match parent width assertNotEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3516,7 +3516,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Vertical reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3533,7 +3533,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Horizontal reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3548,7 +3548,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3563,7 +3563,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_LANDSCAPE); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3585,7 +3585,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is disabled because the app does not match parent height assertNotEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3608,7 +3608,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is enabled because the app matches parent height assertEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3631,7 +3631,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is enabled because the app matches parent width assertEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -4315,7 +4315,7 @@ public class SizeCompatTests extends WindowTestsBase { // Make sure app doesn't jump to top (default tabletop position) when unfolding. assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); // Simulate display fully open after unfolding. @@ -4323,7 +4323,7 @@ public class SizeCompatTests extends WindowTestsBase { doReturn(false).when(mActivity.mDisplayContent).inTransition(); assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); } @@ -5028,7 +5028,7 @@ public class SizeCompatTests extends WindowTestsBase { private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); spyOn(reachabilityOverrides); doReturn(thinLetterboxAllowed).when(reachabilityOverrides) .allowVerticalReachabilityForThinLetterbox(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index ce0d91264063..37d2a7511d98 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -478,7 +478,7 @@ public class WindowTestsBase extends SystemServiceTestsBase { } private WindowState createCommonWindow(WindowState parent, int type, String name) { - final WindowState win = createWindow(parent, type, name); + final WindowState win = newWindowBuilder(name, type).setParent(parent).build(); // Prevent common windows from been IME targets. win.mAttrs.flags |= FLAG_NOT_FOCUSABLE; return win; @@ -502,7 +502,8 @@ public class WindowTestsBase extends SystemServiceTestsBase { } WindowState createNavBarWithProvidedInsets(DisplayContent dc) { - final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, dc, "navbar"); + final WindowState navbar = newWindowBuilder("navbar", TYPE_NAVIGATION_BAR).setDisplay( + dc).build(); final Binder owner = new Binder(); navbar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars()) @@ -513,7 +514,8 @@ public class WindowTestsBase extends SystemServiceTestsBase { } WindowState createStatusBarWithProvidedInsets(DisplayContent dc) { - final WindowState statusBar = createWindow(null, TYPE_STATUS_BAR, dc, "statusBar"); + final WindowState statusBar = newWindowBuilder("statusBar", TYPE_STATUS_BAR).setDisplay( + dc).build(); final Binder owner = new Binder(); statusBar.mAttrs.providedInsets = new InsetsFrameProvider[] { new InsetsFrameProvider(owner, 0, WindowInsets.Type.statusBars()) @@ -575,92 +577,13 @@ public class WindowTestsBase extends SystemServiceTestsBase { WindowState createAppWindow(Task task, int type, String name) { final ActivityRecord activity = createNonAttachedActivityRecord(task.getDisplayContent()); task.addChild(activity, 0); - return createWindow(null, type, activity, name); + return newWindowBuilder(name, type).setWindowToken(activity).build(); } - WindowState createDreamWindow(WindowState parent, int type, String name) { + WindowState createDreamWindow(String name, int type) { final WindowToken token = createWindowToken( mDisplayContent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_DREAM, type); - return createWindow(parent, type, token, name); - } - - // TODO: Move these calls to a builder? - WindowState createWindow(WindowState parent, int type, String name) { - return (parent == null) - ? createWindow(parent, type, mDisplayContent, name) - : createWindow(parent, type, parent.mToken, name); - } - - WindowState createWindow(WindowState parent, int type, String name, int ownerId) { - return (parent == null) - ? createWindow(parent, type, mDisplayContent, name, ownerId) - : createWindow(parent, type, parent.mToken, name, ownerId); - } - - WindowState createWindow(WindowState parent, int windowingMode, int activityType, - int type, DisplayContent dc, String name) { - final WindowToken token = createWindowToken(dc, windowingMode, activityType, type); - return createWindow(parent, type, token, name); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name) { - return createWindow( - parent, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type, dc, name); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name, - int ownerId) { - final WindowToken token = createWindowToken( - dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type); - return createWindow(parent, type, token, name, ownerId); - } - - WindowState createWindow(WindowState parent, int type, DisplayContent dc, String name, - boolean ownerCanAddInternalSystemWindow) { - final WindowToken token = createWindowToken( - dc, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, type); - return createWindow(parent, type, token, name, 0 /* ownerId */, - ownerCanAddInternalSystemWindow); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name) { - return createWindow(parent, type, token, name, 0 /* ownerId */, - false /* ownerCanAddInternalSystemWindow */); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId) { - return createWindow(parent, type, token, name, ownerId, - false /* ownerCanAddInternalSystemWindow */); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId, boolean ownerCanAddInternalSystemWindow) { - return createWindow(parent, type, token, name, ownerId, ownerCanAddInternalSystemWindow, - mIWindow); - } - - WindowState createWindow(WindowState parent, int type, WindowToken token, String name, - int ownerId, boolean ownerCanAddInternalSystemWindow, IWindow iwindow) { - return createWindow(parent, type, token, name, ownerId, UserHandle.getUserId(ownerId), - ownerCanAddInternalSystemWindow, mWm, getTestSession(token), iwindow); - } - - static WindowState createWindow(WindowState parent, int type, WindowToken token, - String name, int ownerId, int userId, boolean ownerCanAddInternalSystemWindow, - WindowManagerService service, Session session, IWindow iWindow) { - SystemServicesTestRule.checkHoldsLock(service.mGlobalLock); - - final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(type); - attrs.setTitle(name); - attrs.packageName = "test"; - - final WindowState w = new WindowState(service, session, iWindow, token, parent, - OP_NONE, attrs, VISIBLE, ownerId, userId, ownerCanAddInternalSystemWindow); - // TODO: Probably better to make this call in the WindowState ctor to avoid errors with - // adding it to the token... - token.addWindow(w); - return w; + return newWindowBuilder(name, type).setWindowToken(token).build(); } static void makeWindowVisible(WindowState... windows) { @@ -1920,11 +1843,14 @@ public class WindowTestsBase extends SystemServiceTestsBase { private final WindowManagerService mWMService; private final SparseArray<IBinder> mTaskAppMap = new SparseArray<>(); private final HashMap<IBinder, WindowState> mAppWindowMap = new HashMap<>(); + private final DisplayContent mDisplayContent; - TestStartingWindowOrganizer(ActivityTaskManagerService service) { + TestStartingWindowOrganizer(ActivityTaskManagerService service, + DisplayContent displayContent) { mAtm = service; mWMService = mAtm.mWindowManager; mAtm.mTaskOrganizerController.registerTaskOrganizer(this); + mDisplayContent = displayContent; } @Override @@ -1933,10 +1859,11 @@ public class WindowTestsBase extends SystemServiceTestsBase { final ActivityRecord activity = ActivityRecord.forTokenLocked(info.appToken); IWindow iWindow = mock(IWindow.class); doReturn(mock(IBinder.class)).when(iWindow).asBinder(); - final WindowState window = WindowTestsBase.createWindow(null, - TYPE_APPLICATION_STARTING, activity, - "Starting window", 0 /* ownerId */, 0 /* userId*/, - false /* internalWindows */, mWMService, createTestSession(mAtm), iWindow); + // WindowToken is already passed, windowTokenCreator is not needed here. + final WindowState window = new WindowTestsBase.WindowStateBuilder("Starting window", + TYPE_APPLICATION_STARTING, mWMService, mDisplayContent, iWindow, + (unused) -> createTestSession(mAtm), + null /* windowTokenCreator */).setWindowToken(activity).build(); activity.mStartingWindow = window; mAppWindowMap.put(info.appToken, window); mTaskAppMap.put(info.taskInfo.taskId, info.appToken); diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 7082f0028a5e..e65e4b05ef98 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -29,6 +29,7 @@ import android.annotation.SuppressAutoDoc; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -1886,6 +1887,34 @@ public class TelecomManager { } /** + * This test API determines the foreground service delegation state for a VoIP app that adds + * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver, + * CallControlCallback, CallEventCallback)}. Foreground Service Delegation allows applications + * to operate in the background starting in Android 14 and is granted by Telecom via a request + * to the ActivityManager. + * + * @param handle of the voip app that is being checked + * @return true if the app has foreground service delegation. Otherwise, false. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR) + @TestApi + public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, + "RemoteException calling ITelecomService#hasForegroundServiceDelegation.", + e); + } + } + return false; + } + + /** * Return the line 1 phone number for given phone account. * * <p>Requires Permission: diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index c85374e0b660..b32379ae4b1e 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -409,4 +409,10 @@ interface ITelecomService { */ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId, String callingPackage); + + /** + * @see TelecomServiceImpl#hasForegroundServiceDelegation + */ + boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle, + String callingPackage); } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 63a12816f783..b7b209b78300 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -3690,8 +3690,8 @@ public final class SatelliteManager { * @param list The list of provisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. - * If the request is successful, {@link OutcomeReceiver#onResult(Object)} - * will return {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3704,7 +3704,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3718,8 +3718,8 @@ public final class SatelliteManager { if (resultData.containsKey(KEY_PROVISION_SATELLITE_TOKENS)) { boolean isUpdated = resultData.getBoolean(KEY_PROVISION_SATELLITE_TOKENS); - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onResult(null))); } else { loge("KEY_REQUEST_PROVISION_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> @@ -3751,8 +3751,8 @@ public final class SatelliteManager { * @param list The list of deprovisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. - * If the request is successful, {@link OutcomeReceiver#onResult(Object)} - * will return {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3765,7 +3765,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void deprovisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3780,7 +3780,7 @@ public final class SatelliteManager { boolean isUpdated = resultData.getBoolean(KEY_DEPROVISION_SATELLITE_TOKENS); executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + callback.onResult(null))); } else { loge("KEY_DEPROVISION_SATELLITE_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index 08b5f38a4655..75bd5d157bb2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.graphics.Rect -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest } } - @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun navBarWindowIsVisibleAtStartAndEnd() {} - @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @FlakyTest(bugId = 291575593) + @Test + override fun entireScreenCovered() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarWindowIsAlwaysVisible() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarLayerPositionAtStartAndEnd() {} /** Transition begins with a split. */ @FlakyTest(bugId = 286952194) @@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest /** Always expand activity is on top of the split. */ @FlakyTest(bugId = 286952194) - @Presubmit @Test fun endsWithAlwaysExpandActivityOnTop() { flicker.assertWmEnd { diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 0ca8f37b239b..e41364595648 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa } @Ignore("Not applicable to this CUJ.") + @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} @FlakyTest(bugId = 342596801) + @Test override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 342596801) + @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index b8f11dcf8970..ad083fa428a9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF } @FlakyTest(bugId = 290767483) - @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 4d7085feb98f..d35c9008e8cb 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -830,6 +830,18 @@ class KeyGestureControllerTests { KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), + TestData( + "META + ALT + 'V' -> Toggle Voice Access", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_V + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, + intArrayOf(KeyEvent.KEYCODE_V), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), ) } @@ -843,6 +855,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) @@ -861,6 +874,7 @@ class KeyGestureControllerTests { com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS ) |