diff options
149 files changed, 5100 insertions, 5871 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index fa9346e89a9f..4bf887962dfa 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -607,9 +607,9 @@ public final class ActivityThread extends ClientTransactionHandler Configuration createdConfig; Configuration overrideConfig; @NonNull - private ActivityWindowInfo mActivityWindowInfo; - @Nullable - private ActivityWindowInfo mLastReportedActivityWindowInfo; + private final ActivityWindowInfo mActivityWindowInfo = new ActivityWindowInfo(); + @NonNull + private final ActivityWindowInfo mLastReportedActivityWindowInfo = new ActivityWindowInfo(); // Used for consolidating configs before sending on to Activity. private final Configuration tmpConfig = new Configuration(); @@ -700,7 +700,7 @@ public final class ActivityThread extends ClientTransactionHandler mSceneTransitionInfo = sceneTransitionInfo; mLaunchedFromBubble = launchedFromBubble; mTaskFragmentToken = taskFragmentToken; - mActivityWindowInfo = activityWindowInfo; + mActivityWindowInfo.set(activityWindowInfo); init(); } @@ -720,7 +720,7 @@ public final class ActivityThread extends ClientTransactionHandler throw new IllegalStateException( "Received config update for non-existing activity"); } - if (activityWindowInfoFlag() && activityWindowInfo == null) { + if (activityWindowInfo == null) { Log.w(TAG, "Received empty ActivityWindowInfo update for r=" + activity); activityWindowInfo = mActivityWindowInfo; } @@ -6063,7 +6063,7 @@ public final class ActivityThread extends ClientTransactionHandler target.createdConfig = config.getGlobalConfiguration(); target.overrideConfig = config.getOverrideConfiguration(); target.pendingConfigChanges |= configChanges; - target.mActivityWindowInfo = activityWindowInfo; + target.mActivityWindowInfo.set(activityWindowInfo); } return scheduleRelaunch ? target : null; @@ -6257,7 +6257,7 @@ public final class ActivityThread extends ClientTransactionHandler } r.startsNotResumed = startsNotResumed; r.overrideConfig = overrideConfig; - r.mActivityWindowInfo = activityWindowInfo; + r.mActivityWindowInfo.set(activityWindowInfo); handleLaunchActivity(r, pendingActions, mLastReportedDeviceId, customIntent); } @@ -6759,7 +6759,7 @@ public final class ActivityThread extends ClientTransactionHandler // Perform updates. r.overrideConfig = overrideConfig; - r.mActivityWindowInfo = activityWindowInfo; + r.mActivityWindowInfo.set(activityWindowInfo); final ViewRootImpl viewRoot = r.activity.mDecor != null ? r.activity.mDecor.getViewRootImpl() : null; @@ -6792,11 +6792,10 @@ public final class ActivityThread extends ClientTransactionHandler if (!activityWindowInfoFlag()) { return; } - if (r.mActivityWindowInfo == null - || r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) { + if (r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) { return; } - r.mLastReportedActivityWindowInfo = r.mActivityWindowInfo; + r.mLastReportedActivityWindowInfo.set(r.mActivityWindowInfo); ClientTransactionListenerController.getInstance().onActivityWindowInfoChanged(r.token, r.mActivityWindowInfo); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7337a7c3b2c3..d7b9a2c46c9b 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -24,6 +24,7 @@ import static android.app.admin.DevicePolicyResources.UNDEFINED; import static android.graphics.drawable.Icon.TYPE_URI; import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; import static android.app.Flags.evenlyDividedCallStyleActionLayout; +import static android.app.Flags.updateRankingTime; import static java.util.Objects.requireNonNull; @@ -339,8 +340,9 @@ public class Notification implements Parcelable /** * The creation time of the notification + * @hide */ - private long creationTime; + public long creationTime; /** * The resource id of a drawable to use as the icon in the status bar. @@ -2578,7 +2580,11 @@ public class Notification implements Parcelable public Notification() { this.when = System.currentTimeMillis(); - this.creationTime = System.currentTimeMillis(); + if (updateRankingTime()) { + creationTime = when; + } else { + this.creationTime = System.currentTimeMillis(); + } this.priority = PRIORITY_DEFAULT; } @@ -2589,6 +2595,9 @@ public class Notification implements Parcelable public Notification(Context context, int icon, CharSequence tickerText, long when, CharSequence contentTitle, CharSequence contentText, Intent contentIntent) { + if (updateRankingTime()) { + creationTime = when; + } new Builder(context) .setWhen(when) .setSmallIcon(icon) @@ -2618,7 +2627,11 @@ public class Notification implements Parcelable this.icon = icon; this.tickerText = tickerText; this.when = when; - this.creationTime = System.currentTimeMillis(); + if (updateRankingTime()) { + creationTime = when; + } else { + this.creationTime = System.currentTimeMillis(); + } } /** @@ -6843,7 +6856,9 @@ public class Notification implements Parcelable } } - mN.creationTime = System.currentTimeMillis(); + if (!updateRankingTime()) { + mN.creationTime = System.currentTimeMillis(); + } // lazy stuff from mContext; see comment in Builder(Context, Notification) Notification.addFieldsFromContext(mContext, mN); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 66ec865092f7..103af4bfa760 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -66,6 +66,8 @@ import android.companion.ICompanionDeviceManager; import android.companion.virtual.IVirtualDeviceManager; import android.companion.virtual.VirtualDeviceManager; import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.ClipboardManager; import android.content.ContentCaptureOptions; import android.content.Context; @@ -196,6 +198,7 @@ import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.StatsFrameworkInitializer; import android.os.SystemConfigManager; +import android.os.SystemProperties; import android.os.SystemUpdateManager; import android.os.SystemVibrator; import android.os.SystemVibratorManager; @@ -285,6 +288,18 @@ public final class SystemServiceRegistry { /** @hide */ public static boolean sEnableServiceNotFoundWtf = false; + /** + * Starting with {@link VANILLA_ICE_CREAM}, Telephony feature flags + * (e.g. {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}) are being checked before + * returning managers that depend on them. If the feature is missing, + * {@link Context#getSystemService} will return null. + * + * This change is specific to VcnManager. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + static final long ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN = 330902016; + // Service registry information. // This information is never changed once static initialization has completed. private static final Map<Class<?>, String> SYSTEM_SERVICE_NAMES = @@ -450,8 +465,9 @@ public final class SystemServiceRegistry { new CachedServiceFetcher<VcnManager>() { @Override public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { - if (!ctx.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + if (shouldCheckTelephonyFeatures() + && !ctx.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { return null; } @@ -1748,6 +1764,22 @@ public final class SystemServiceRegistry { return manager.hasSystemFeature(featureName); } + // Suppressing AndroidFrameworkCompatChange because we're querying vendor + // partition SDK level, not application's target SDK version (which BTW we + // also check through Compatibility framework a few lines below). + @SuppressWarnings("AndroidFrameworkCompatChange") + private static boolean shouldCheckTelephonyFeatures() { + // Check SDK version of the vendor partition. Pre-V devices might have + // incorrectly under-declared telephony features. + final int vendorApiLevel = SystemProperties.getInt( + "ro.vendor.api_level", Build.VERSION.DEVICE_INITIAL_SDK_INT); + if (vendorApiLevel < Build.VERSION_CODES.VANILLA_ICE_CREAM) return false; + + // Check SDK version of the client app. Apps targeting pre-V SDK might + // have not checked for existence of these features. + return Compatibility.isChangeEnabled(ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN); + } + /** * Gets a system service from a given context. * @hide diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index e9a746022a75..29ffdc5f4a55 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -71,4 +71,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "update_ranking_time" + namespace: "systemui" + description: "Updates notification sorting criteria to highlight new content while maintaining stability" + bug: "326016985" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java index 7383d07c82e9..c55b0f110b3b 100644 --- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java +++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java @@ -122,7 +122,7 @@ public class ClientTransactionListenerController { } for (Object activityWindowInfoChangedListener : activityWindowInfoChangedListeners) { ((BiConsumer<IBinder, ActivityWindowInfo>) activityWindowInfoChangedListener) - .accept(activityToken, activityWindowInfo); + .accept(activityToken, new ActivityWindowInfo(activityWindowInfo)); } } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 5e00b7a798d8..2c26389071ce 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1086,7 +1086,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.registerDevicePresenceListenerService(deviceAddress, + mService.legacyStartObservingDevicePresence(deviceAddress, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1128,7 +1128,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.unregisterDevicePresenceListenerService(deviceAddress, + mService.legacyStopObservingDevicePresence(deviceAddress, mContext.getPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1328,7 +1328,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int associationId) { try { - mService.notifyDeviceAppeared(associationId); + mService.notifySelfManagedDeviceAppeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1350,7 +1350,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int associationId) { try { - mService.notifyDeviceDisappeared(associationId); + mService.notifySelfManagedDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 57d59e5e5bf0..1b00f90e1fb3 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -59,12 +59,16 @@ interface ICompanionDeviceManager { int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStopObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId); @@ -93,9 +97,11 @@ interface ICompanionDeviceManager { @EnforcePermission("USE_COMPANION_TRANSPORTS") void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); - void notifyDeviceAppeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceAppeared(int associationId); - void notifyDeviceDisappeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceDisappeared(int associationId); PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId, int associationId); @@ -135,10 +141,4 @@ interface ICompanionDeviceManager { byte[] getBackupPayload(int userId); void applyRestoredPayload(in byte[] payload, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); } diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index bb0498ed6a78..229d1195f6b7 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -9,14 +9,6 @@ flag { flag { namespace: "haptics" - name: "haptics_customization_enabled" - is_exported: true - description: "Enables the haptics customization feature" - bug: "241918098" -} - -flag { - namespace: "haptics" name: "haptic_feedback_vibration_oem_customization_enabled" description: "Enables OEMs/devices to customize vibrations for haptic feedback" # Make read only. This is because the flag is used only once, and this could happen before diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3c61854c89f0..85d3688a9a1e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -104,7 +104,7 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_B import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; -import static android.view.accessibility.Flags.fixMergedContentChangeEvent; +import static android.view.accessibility.Flags.fixMergedContentChangeEventV2; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly; @@ -11796,7 +11796,7 @@ public final class ViewRootImpl implements ViewParent, } if (mSource != null) { - if (fixMergedContentChangeEvent()) { + if (fixMergedContentChangeEventV2()) { View newSource = getCommonPredecessor(mSource, source); if (newSource != null) { newSource = newSource.getSelfOrParentImportantForA11y(); diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 91bd4ea0bc87..eefc72b82c24 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -56,9 +56,12 @@ flag { flag { namespace: "accessibility" - name: "fix_merged_content_change_event" + name: "fix_merged_content_change_event_v2" description: "Fixes event type and source of content change event merged in ViewRootImpl" bug: "277305460" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index b6066ba5560f..9c63d0dd746a 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -1297,6 +1297,17 @@ public class ConversationLayout extends FrameLayout */ @Nullable private Drawable resolveAvatarImageForOneToOne(Icon conversationIcon) { + final Drawable conversationIconDrawable = + tryLoadingSizeRestrictedIconForOneToOne(conversationIcon); + if (conversationIconDrawable != null) { + return conversationIconDrawable; + } + // when size restricted icon loading fails, we fallback to icons load drawable. + return loadDrawableFromIcon(conversationIcon); + } + + @Nullable + private Drawable tryLoadingSizeRestrictedIconForOneToOne(Icon conversationIcon) { try { return mConversationIconView.loadSizeRestrictedIcon(conversationIcon); } catch (Exception ex) { @@ -1309,6 +1320,11 @@ public class ConversationLayout extends FrameLayout */ @Nullable private Drawable resolveAvatarImageForFacePile(Icon conversationIcon) { + return loadDrawableFromIcon(conversationIcon); + } + + @Nullable + private Drawable loadDrawableFromIcon(Icon conversationIcon) { try { return conversationIcon.loadDrawable(getContext()); } catch (Exception ex) { diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 927c67cb1d36..be0669c42d44 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -34,9 +34,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.annotation.NonNull; @@ -67,7 +69,6 @@ import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Bundle; import android.os.IBinder; -import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.util.DisplayMetrics; @@ -90,7 +91,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -123,9 +123,7 @@ public class ActivityThreadTest { @Rule(order = 1) public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - @Mock - private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener; - + private ActivityWindowInfoListener mActivityWindowInfoListener; private WindowTokenClientController mOriginalWindowTokenClientController; private Configuration mOriginalAppConfig; @@ -140,6 +138,7 @@ public class ActivityThreadTest { mOriginalWindowTokenClientController = WindowTokenClientController.getInstance(); mOriginalAppConfig = new Configuration(ActivityThread.currentActivityThread() .getConfiguration()); + mActivityWindowInfoListener = spy(new ActivityWindowInfoListener()); } @After @@ -808,96 +807,107 @@ public class ActivityThreadTest { @Test public void testActivityWindowInfoChanged_activityLaunch() { mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); - ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); final Activity activity = mActivityTestRule.launchActivity(new Intent()); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + mActivityWindowInfoListener.await(); final ActivityClientRecord activityClientRecord = getActivityClientRecord(activity); - verify(mActivityWindowInfoListener).accept(activityClientRecord.token, + // In case the system change the window after launch, there can be more than one callback. + verify(mActivityWindowInfoListener, atLeastOnce()).accept(activityClientRecord.token, activityClientRecord.getActivityWindowInfo()); } @Test - public void testActivityWindowInfoChanged_activityRelaunch() throws RemoteException { + public void testActivityWindowInfoChanged_activityRelaunch() { mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); - ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); final Activity activity = mActivityTestRule.launchActivity(new Intent()); - final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); - appThread.scheduleTransaction(newRelaunchResumeTransaction(activity)); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + mActivityWindowInfoListener.await(); final ActivityClientRecord activityClientRecord = getActivityClientRecord(activity); - // The same ActivityWindowInfo won't trigger duplicated callback. - verify(mActivityWindowInfoListener).accept(activityClientRecord.token, - activityClientRecord.getActivityWindowInfo()); - - final Configuration currentConfig = activity.getResources().getConfiguration(); - final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); - activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000), - new Rect(0, 0, 1000, 1000)); - final ActivityRelaunchItem relaunchItem = ActivityRelaunchItem.obtain( - activity.getActivityToken(), null, null, 0, - new MergedConfiguration(currentConfig, currentConfig), - false /* preserveWindow */, activityWindowInfo); - final ClientTransaction transaction = newTransaction(activity); - transaction.addTransactionItem(relaunchItem); - appThread.scheduleTransaction(transaction); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - - verify(mActivityWindowInfoListener).accept(activityClientRecord.token, - activityWindowInfo); + // Run on main thread to avoid racing from updating from window relayout. + final ActivityThread activityThread = activity.getActivityThread(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // Try relaunch with the same ActivityWindowInfo + clearInvocations(mActivityWindowInfoListener); + activityThread.executeTransaction(newRelaunchResumeTransaction(activity)); + + // The same ActivityWindowInfo won't trigger duplicated callback. + verify(mActivityWindowInfoListener, never()).accept(activityClientRecord.token, + activityClientRecord.getActivityWindowInfo()); + + // Try relaunch with different ActivityWindowInfo + final Configuration currentConfig = activity.getResources().getConfiguration(); + final ActivityWindowInfo newInfo = new ActivityWindowInfo(); + newInfo.set(true /* isEmbedded */, new Rect(0, 0, 1000, 2000), + new Rect(0, 0, 1000, 1000)); + final ActivityRelaunchItem relaunchItem = ActivityRelaunchItem.obtain( + activity.getActivityToken(), null, null, 0, + new MergedConfiguration(currentConfig, currentConfig), + false /* preserveWindow */, newInfo); + final ClientTransaction transaction = newTransaction(activity); + transaction.addTransactionItem(relaunchItem); + + clearInvocations(mActivityWindowInfoListener); + activityThread.executeTransaction(transaction); + + // Trigger callback with a different ActivityWindowInfo + verify(mActivityWindowInfoListener).accept(activityClientRecord.token, newInfo); + }); } @Test - public void testActivityWindowInfoChanged_activityConfigurationChanged() - throws RemoteException { + public void testActivityWindowInfoChanged_activityConfigurationChanged() { mSetFlagsRule.enableFlags(FLAG_ACTIVITY_WINDOW_INFO_FLAG); - ClientTransactionListenerController.getInstance().registerActivityWindowInfoChangedListener( mActivityWindowInfoListener); final Activity activity = mActivityTestRule.launchActivity(new Intent()); - final IApplicationThread appThread = activity.getActivityThread().getApplicationThread(); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + mActivityWindowInfoListener.await(); - clearInvocations(mActivityWindowInfoListener); - final Configuration config = new Configuration(activity.getResources().getConfiguration()); - config.seq++; - final Rect taskBounds = new Rect(0, 0, 1000, 2000); - final Rect taskFragmentBounds = new Rect(0, 0, 1000, 1000); - final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); - activityWindowInfo.set(true /* isEmbedded */, taskBounds, taskFragmentBounds); - final ActivityConfigurationChangeItem activityConfigurationChangeItem = - ActivityConfigurationChangeItem.obtain( - activity.getActivityToken(), config, activityWindowInfo); - final ClientTransaction transaction = newTransaction(activity); - transaction.addTransactionItem(activityConfigurationChangeItem); - appThread.scheduleTransaction(transaction); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - - verify(mActivityWindowInfoListener).accept(activity.getActivityToken(), - activityWindowInfo); - - clearInvocations(mActivityWindowInfoListener); - final ActivityWindowInfo activityWindowInfo2 = new ActivityWindowInfo(); - activityWindowInfo2.set(true /* isEmbedded */, taskBounds, taskFragmentBounds); - config.seq++; - final ActivityConfigurationChangeItem activityConfigurationChangeItem2 = - ActivityConfigurationChangeItem.obtain( - activity.getActivityToken(), config, activityWindowInfo2); - final ClientTransaction transaction2 = newTransaction(activity); - transaction2.addTransactionItem(activityConfigurationChangeItem2); - appThread.scheduleTransaction(transaction); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - - // The same ActivityWindowInfo won't trigger duplicated callback. - verify(mActivityWindowInfoListener, never()).accept(any(), any()); + final ActivityThread activityThread = activity.getActivityThread(); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + // Trigger callback with different ActivityWindowInfo + final Configuration config = new Configuration(activity.getResources() + .getConfiguration()); + config.seq++; + final Rect taskBounds = new Rect(0, 0, 1000, 2000); + final Rect taskFragmentBounds = new Rect(0, 0, 1000, 1000); + final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); + activityWindowInfo.set(true /* isEmbedded */, taskBounds, taskFragmentBounds); + final ActivityConfigurationChangeItem activityConfigurationChangeItem = + ActivityConfigurationChangeItem.obtain( + activity.getActivityToken(), config, activityWindowInfo); + final ClientTransaction transaction = newTransaction(activity); + transaction.addTransactionItem(activityConfigurationChangeItem); + + clearInvocations(mActivityWindowInfoListener); + activityThread.executeTransaction(transaction); + + // Trigger callback with a different ActivityWindowInfo + verify(mActivityWindowInfoListener).accept(activity.getActivityToken(), + activityWindowInfo); + + // Try callback with the same ActivityWindowInfo + final ActivityWindowInfo activityWindowInfo2 = + new ActivityWindowInfo(activityWindowInfo); + config.seq++; + final ActivityConfigurationChangeItem activityConfigurationChangeItem2 = + ActivityConfigurationChangeItem.obtain( + activity.getActivityToken(), config, activityWindowInfo2); + final ClientTransaction transaction2 = newTransaction(activity); + transaction2.addTransactionItem(activityConfigurationChangeItem2); + + clearInvocations(mActivityWindowInfoListener); + activityThread.executeTransaction(transaction); + + // The same ActivityWindowInfo won't trigger duplicated callback. + verify(mActivityWindowInfoListener, never()).accept(any(), any()); + }); } /** @@ -958,10 +968,12 @@ public class ActivityThreadTest { @NonNull private static ClientTransaction newRelaunchResumeTransaction(@NonNull Activity activity) { final Configuration currentConfig = activity.getResources().getConfiguration(); + final ActivityWindowInfo activityWindowInfo = getActivityClientRecord(activity) + .getActivityWindowInfo(); final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain( activity.getActivityToken(), null, null, 0, new MergedConfiguration(currentConfig, currentConfig), - false /* preserveWindow */, new ActivityWindowInfo()); + false /* preserveWindow */, activityWindowInfo); final ResumeActivityItem resumeStateRequest = ResumeActivityItem.obtain(activity.getActivityToken(), true /* isForward */, false /* shouldSendCompatFakeFocus*/); @@ -1127,4 +1139,28 @@ public class ActivityThreadTest { return mPipEnterSkipped; } } + + public static class ActivityWindowInfoListener implements + BiConsumer<IBinder, ActivityWindowInfo> { + + CountDownLatch mCallbackLatch = new CountDownLatch(1); + + @Override + public void accept(@NonNull IBinder activityToken, + @NonNull ActivityWindowInfo activityWindowInfo) { + mCallbackLatch.countDown(); + } + + /** + * When the test is expecting to receive a callback, waits until the callback is triggered. + */ + void await() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + try { + mCallbackLatch.await(TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java index de7244d49834..71c068d7ad46 100644 --- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java +++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java @@ -149,7 +149,7 @@ public class DateIntervalFormatTest { FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); assertEquals("19.–22.01.2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); - assertEquals("19.01. – 22.04.2009", + assertEquals("19.01.\u2009–\u200922.04.2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH, FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE)); assertEquals("19.01.2009\u2009\u2013\u200909.02.2012", @@ -220,10 +220,10 @@ public class DateIntervalFormatTest { formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, 0)); assertEquals("19.–22. Jan. 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL)); - assertEquals("Mo., 19. – Do., 22. Jan. 2009", + assertEquals("Mo., 19.\u2009–\u2009Do., 22. Jan. 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL)); - assertEquals("Montag, 19. – Donnerstag, 22. Januar 2009", + assertEquals("Montag, 19.\u2009–\u2009Donnerstag, 22. Januar 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY)); assertEquals("19. Januar\u2009\u2013\u200922. April 2009", diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index 488f017872b1..ff280555a3a1 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -1258,7 +1258,8 @@ public class ResolverActivityTest { @Test public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() { mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE, + android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); markWorkProfileUserAvailable(); setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE); Intent sendIntent = createSendImageIntent(); @@ -1281,7 +1282,8 @@ public class ResolverActivityTest { @Test public void testTriggerFromWorkProfile_inSingleUserMode() { mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, - android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE, + android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); markWorkProfileUserAvailable(); setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle); Intent sendIntent = createSendImageIntent(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index cae232e54f3c..b8ac19189f60 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -23,9 +23,11 @@ import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET; import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET; +import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; @@ -45,8 +47,10 @@ import android.hardware.display.DisplayManager; import android.os.IBinder; import android.util.TypedValue; import android.view.Gravity; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.widget.FrameLayout; @@ -56,23 +60,30 @@ import android.window.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; import android.window.WindowContainerTransaction; +import androidx.annotation.GuardedBy; import androidx.annotation.IdRes; import androidx.annotation.NonNull; +import androidx.window.extensions.core.util.function.Consumer; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; import java.util.Objects; +import java.util.concurrent.Executor; /** * Manages the rendering and interaction of the divider. */ -class DividerPresenter { +class DividerPresenter implements View.OnTouchListener { private static final String WINDOW_NAME = "AE Divider"; + private static final int VEIL_LAYER = 0; + private static final int DIVIDER_LAYER = 1; // TODO(b/327067596) Update based on UX guidance. private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK); + private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK); + private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY); @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f; @VisibleForTesting @@ -80,11 +91,23 @@ class DividerPresenter { @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + private final int mTaskId; + + @NonNull + private final Object mLock = new Object(); + + @NonNull + private final DragEventCallback mDragEventCallback; + + @NonNull + private final Executor mCallbackExecutor; + /** * The {@link Properties} of the divider. This field is {@code null} when no divider should be * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface * is not available. */ + @GuardedBy("mLock") @Nullable @VisibleForTesting Properties mProperties; @@ -94,6 +117,7 @@ class DividerPresenter { * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or * updated when {@link #mProperties} is changed. */ + @GuardedBy("mLock") @Nullable @VisibleForTesting Renderer mRenderer; @@ -102,10 +126,26 @@ class DividerPresenter { * The owner TaskFragment token of the decor surface. The decor surface is placed right above * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. */ + @GuardedBy("mLock") @Nullable @VisibleForTesting IBinder mDecorSurfaceOwner; + /** + * The current divider position relative to the Task bounds. For vertical split (left-to-right + * or right-to-left), it is the x coordinate in the task window, and for horizontal split + * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window. + */ + @GuardedBy("mLock") + private int mDividerPosition; + + DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback, + @NonNull Executor callbackExecutor) { + mTaskId = taskId; + mDragEventCallback = dragEventCallback; + mCallbackExecutor = callbackExecutor; + } + /** Updates the divider when external conditions are changed. */ void updateDivider( @NonNull WindowContainerTransaction wct, @@ -115,58 +155,65 @@ class DividerPresenter { return; } - // Clean up the decor surface if top SplitContainer is null. - if (topSplitContainer == null) { - removeDecorSurfaceAndDivider(wct); - return; - } + synchronized (mLock) { + // Clean up the decor surface if top SplitContainer is null. + if (topSplitContainer == null) { + removeDecorSurfaceAndDivider(wct); + return; + } - // Clean up the decor surface if DividerAttributes is null. - final DividerAttributes dividerAttributes = - topSplitContainer.getCurrentSplitAttributes().getDividerAttributes(); - if (dividerAttributes == null) { - removeDecorSurfaceAndDivider(wct); - return; - } + // Clean up the decor surface if DividerAttributes is null. + final DividerAttributes dividerAttributes = + topSplitContainer.getCurrentSplitAttributes().getDividerAttributes(); + if (dividerAttributes == null) { + removeDecorSurfaceAndDivider(wct); + return; + } - if (topSplitContainer.getCurrentSplitAttributes().getSplitType() - instanceof SplitAttributes.SplitType.ExpandContainersSplitType) { - // No divider is needed for ExpandContainersSplitType. - removeDivider(); - return; - } + if (topSplitContainer.getCurrentSplitAttributes().getSplitType() + instanceof SplitAttributes.SplitType.ExpandContainersSplitType) { + // No divider is needed for ExpandContainersSplitType. + removeDivider(); + return; + } - // Skip updating when the TFs have not been updated to match the SplitAttributes. - if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty() - || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) { - return; - } + // Skip updating when the TFs have not been updated to match the SplitAttributes. + if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty() + || topSplitContainer.getSecondaryContainer().getLastRequestedBounds() + .isEmpty()) { + return; + } - final SurfaceControl decorSurface = parentInfo.getDecorSurface(); - if (decorSurface == null) { - // Clean up when the decor surface is currently unavailable. - removeDivider(); - // Request to create the decor surface - createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); - return; - } + final SurfaceControl decorSurface = parentInfo.getDecorSurface(); + if (decorSurface == null) { + // Clean up when the decor surface is currently unavailable. + removeDivider(); + // Request to create the decor surface + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + return; + } - // make the top primary container the owner of the decor surface. - if (!Objects.equals(mDecorSurfaceOwner, - topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) { - createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); - } + // make the top primary container the owner of the decor surface. + if (!Objects.equals(mDecorSurfaceOwner, + topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) { + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + } - updateProperties( - new Properties( - parentInfo.getConfiguration(), - dividerAttributes, - decorSurface, - getInitialDividerPosition(topSplitContainer), - isVerticalSplit(topSplitContainer), - parentInfo.getDisplayId())); + updateProperties( + new Properties( + parentInfo.getConfiguration(), + dividerAttributes, + decorSurface, + getInitialDividerPosition(topSplitContainer), + isVerticalSplit(topSplitContainer), + isReversedLayout( + topSplitContainer.getCurrentSplitAttributes(), + parentInfo.getConfiguration()), + parentInfo.getDisplayId())); + } } + @GuardedBy("mLock") private void updateProperties(@NonNull Properties properties) { if (Properties.equalsForDivider(mProperties, properties)) { return; @@ -176,16 +223,16 @@ class DividerPresenter { if (mRenderer == null) { // Create a new renderer when a renderer doesn't exist yet. - mRenderer = new Renderer(); + mRenderer = new Renderer(mProperties, this); } else if (!Properties.areSameSurfaces( previousProperties.mDecorSurface, mProperties.mDecorSurface) || previousProperties.mDisplayId != mProperties.mDisplayId) { // Release and recreate the renderer if the decor surface or the display has changed. mRenderer.release(); - mRenderer = new Renderer(); + mRenderer = new Renderer(mProperties, this); } else { // Otherwise, update the renderer for the new properties. - mRenderer.update(); + mRenderer.update(mProperties); } } @@ -195,6 +242,7 @@ class DividerPresenter { * * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. */ + @GuardedBy("mLock") private void createOrMoveDecorSurface( @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( @@ -204,6 +252,7 @@ class DividerPresenter { mDecorSurfaceOwner = container.getTaskFragmentToken(); } + @GuardedBy("mLock") private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { if (mDecorSurfaceOwner != null) { final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( @@ -215,6 +264,7 @@ class DividerPresenter { removeDivider(); } + @GuardedBy("mLock") private void removeDivider() { if (mRenderer != null) { mRenderer.release(); @@ -238,7 +288,7 @@ class DividerPresenter { private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) { final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection(); - switch(layoutDirection) { + switch (layoutDirection) { case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: case SplitAttributes.LayoutDirection.LOCALE: @@ -251,12 +301,6 @@ class DividerPresenter { } } - private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) { - if (sc != null) { - sc.release(); - } - } - private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { int dividerWidthDp = dividerAttributes.getWidthDp(); return convertDpToPixel(dividerWidthDp); @@ -388,6 +432,227 @@ class DividerPresenter { .build(); } + @Override + public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) { + synchronized (mLock) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + mDividerPosition = calculateDividerPosition( + event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes, + mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout); + mRenderer.setDividerPosition(mDividerPosition); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartDragging(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onFinishDragging(); + break; + case MotionEvent.ACTION_MOVE: + onDrag(); + break; + default: + break; + } + } + + // Returns false so that the default button click callback is still triggered, i.e. the + // button UI transitions into the "pressed" state. + return false; + } + + @GuardedBy("mLock") + private void onStartDragging() { + mRenderer.mIsDragging = true; + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + mRenderer.showVeils(t); + final IBinder decorSurfaceOwner = mDecorSurfaceOwner; + + // Callbacks must be executed on the executor to release mLock and prevent deadlocks. + mCallbackExecutor.execute(() -> { + mDragEventCallback.onStartDragging( + wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, true /* boosted */, t)); + }); + } + + @GuardedBy("mLock") + private void onDrag() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + t.apply(); + } + + @GuardedBy("mLock") + private void onFinishDragging() { + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + mRenderer.updateSurface(t); + mRenderer.hideVeils(t); + final IBinder decorSurfaceOwner = mDecorSurfaceOwner; + + // Callbacks must be executed on the executor to release mLock and prevent deadlocks. + mCallbackExecutor.execute(() -> { + mDragEventCallback.onFinishDragging( + mTaskId, + wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, false /* boosted */, t)); + }); + mRenderer.mIsDragging = false; + } + + private static void setDecorSurfaceBoosted( + @NonNull WindowContainerTransaction wct, + @Nullable IBinder decorSurfaceOwner, + boolean boosted, + @NonNull SurfaceControl.Transaction clientTransaction) { + if (decorSurfaceOwner == null) { + return; + } + wct.addTaskFragmentOperation( + decorSurfaceOwner, + new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED) + .setBooleanValue(boosted) + .setSurfaceTransaction(clientTransaction) + .build() + ); + } + + /** Calculates the new divider position based on the touch event and divider attributes. */ + @VisibleForTesting + static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds, + int dividerWidthPx, @NonNull DividerAttributes dividerAttributes, + boolean isVerticalSplit, boolean isReversedLayout) { + // The touch event is in display space. Converting it into the task window space. + final int touchPositionInTaskSpace = isVerticalSplit + ? (int) (event.getRawX()) - taskBounds.left + : (int) (event.getRawY()) - taskBounds.top; + + // Assuming that the touch position is at the center of the divider bar, so the divider + // position is offset by half of the divider width. + int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2; + + // Limit the divider position to the min and max ratios set in DividerAttributes. + // TODO(b/327536303) Handle when the divider is dragged to the edge. + dividerPosition = Math.max(dividerPosition, calculateMinPosition( + taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout)); + dividerPosition = Math.min(dividerPosition, calculateMaxPosition( + taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout)); + return dividerPosition; + } + + /** Calculates the min position of the divider that the user is allowed to drag to. */ + @VisibleForTesting + static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx, + @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, + boolean isReversedLayout) { + // The usable size is the task window size minus the divider bar width. This is shared + // between the primary and secondary containers based on the split ratio. + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + return (int) (isReversedLayout + ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio() + : usableSize * dividerAttributes.getPrimaryMinRatio()); + } + + /** Calculates the max position of the divider that the user is allowed to drag to. */ + @VisibleForTesting + static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx, + @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit, + boolean isReversedLayout) { + // The usable size is the task window size minus the divider bar width. This is shared + // between the primary and secondary containers based on the split ratio. + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + return (int) (isReversedLayout + ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio() + : usableSize * dividerAttributes.getPrimaryMaxRatio()); + } + + /** + * Returns the new split ratio of the {@link SplitContainer} based on the current divider + * position. + */ + float calculateNewSplitRatio(@NonNull SplitContainer topSplitContainer) { + synchronized (mLock) { + return calculateNewSplitRatio( + topSplitContainer, + mDividerPosition, + mProperties.mConfiguration.windowConfiguration.getBounds(), + mRenderer.mDividerWidthPx, + mProperties.mIsVerticalSplit, + mProperties.mIsReversedLayout); + } + } + + /** + * Returns the new split ratio of the {@link SplitContainer} based on the current divider + * position. + * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio. + * @param dividerPosition the divider position. See {@link #mDividerPosition}. + * @param taskBounds the task bounds + * @param dividerWidthPx the width of the divider in pixels. + * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the + * split is a horizontal split. See + * {@link #isVerticalSplit(SplitContainer)}. + * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or + * bottom-to-top. If {@code false}, the split is not reversed, i.e. + * left-to-right or top-to-bottom. See + * {@link SplitAttributesHelper#isReversedLayout} + * @return the computed split ratio of the primary container. + */ + @VisibleForTesting + static float calculateNewSplitRatio( + @NonNull SplitContainer topSplitContainer, + int dividerPosition, + @NonNull Rect taskBounds, + int dividerWidthPx, + boolean isVerticalSplit, + boolean isReversedLayout) { + final int usableSize = isVerticalSplit + ? taskBounds.width() - dividerWidthPx + : taskBounds.height() - dividerWidthPx; + + final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); + final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds(); + + float newRatio; + if (isVerticalSplit) { + final int newPrimaryWidth = isReversedLayout + ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx)) + : (dividerPosition - origPrimaryBounds.left); + newRatio = 1.0f * newPrimaryWidth / usableSize; + } else { + final int newPrimaryHeight = isReversedLayout + ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx)) + : (dividerPosition - origPrimaryBounds.top); + newRatio = 1.0f * newPrimaryHeight / usableSize; + } + return newRatio; + } + + /** Callbacks for drag events */ + interface DragEventCallback { + /** + * Called when the user starts dragging the divider. Callbacks are executed on + * {@link #mCallbackExecutor}. + * + * @param action additional action that should be applied to the + * {@link WindowContainerTransaction} + */ + void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action); + + /** + * Called when the user finishes dragging the divider. Callbacks are executed on + * {@link #mCallbackExecutor}. + * + * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to. + * @param action additional action that should be applied to the + * {@link WindowContainerTransaction} + */ + void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action); + } + /** * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on * these properties. When any value is updated, the divider is re-rendered. The Properties @@ -411,6 +676,7 @@ class DividerPresenter { private final boolean mIsVerticalSplit; private final int mDisplayId; + private final boolean mIsReversedLayout; @VisibleForTesting Properties( @@ -419,12 +685,14 @@ class DividerPresenter { @NonNull SurfaceControl decorSurface, int initialDividerPosition, boolean isVerticalSplit, + boolean isReversedLayout, int displayId) { mConfiguration = configuration; mDividerAttributes = dividerAttributes; mDecorSurface = decorSurface; mInitialDividerPosition = initialDividerPosition; mIsVerticalSplit = isVerticalSplit; + mIsReversedLayout = isReversedLayout; mDisplayId = displayId; } @@ -445,7 +713,8 @@ class DividerPresenter { && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) && a.mInitialDividerPosition == b.mInitialDividerPosition && a.mIsVerticalSplit == b.mIsVerticalSplit - && a.mDisplayId == b.mDisplayId; + && a.mDisplayId == b.mDisplayId + && a.mIsReversedLayout == b.mIsReversedLayout; } private static boolean areSameSurfaces( @@ -472,7 +741,7 @@ class DividerPresenter { * recreated. When other fields in the Properties are changed, the renderer is updated. */ @VisibleForTesting - class Renderer { + static class Renderer { @NonNull private final SurfaceControl mDividerSurface; @NonNull @@ -481,10 +750,21 @@ class DividerPresenter { private final SurfaceControlViewHost mViewHost; @NonNull private final FrameLayout mDividerLayout; - private final int mDividerWidthPx; - - private Renderer() { - mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); + @NonNull + private final View.OnTouchListener mListener; + @NonNull + private Properties mProperties; + private int mDividerWidthPx; + @Nullable + private SurfaceControl mPrimaryVeil; + @Nullable + private SurfaceControl mSecondaryVeil; + private boolean mIsDragging; + private int mDividerPosition; + + private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) { + mProperties = properties; + mListener = listener; mDividerSurface = createChildSurface("DividerSurface", true /* visible */); mWindowlessWindowManager = new WindowlessWindowManager( @@ -503,36 +783,63 @@ class DividerPresenter { } /** Updates the divider when properties are changed */ + private void update(@NonNull Properties newProperties) { + mProperties = newProperties; + update(); + } + + /** Updates the divider when initializing or when properties are changed */ @VisibleForTesting void update() { + mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); + mDividerPosition = mProperties.mInitialDividerPosition; mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); - updateSurface(); + // TODO handle synchronization between surface transactions and WCT. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + updateSurface(t); updateLayout(); - updateDivider(); + updateDivider(t); + t.apply(); } @VisibleForTesting void release() { mViewHost.release(); // TODO handle synchronization between surface transactions and WCT. - new SurfaceControl.Transaction().remove(mDividerSurface).apply(); - safeReleaseSurfaceControl(mDividerSurface); + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.remove(mDividerSurface); + removeVeils(t); + t.apply(); } - private void updateSurface() { + private void setDividerPosition(int dividerPosition) { + mDividerPosition = dividerPosition; + } + + /** + * Updates the positions and crops of the divider surface and veil surfaces. This method + * should be called when {@link #mProperties} is changed or while dragging to update the + * position of the divider surface and the veil surfaces. + */ + private void updateSurface(@NonNull SurfaceControl.Transaction t) { final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); - // TODO handle synchronization between surface transactions and WCT. - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); if (mProperties.mIsVerticalSplit) { - t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f); + t.setPosition(mDividerSurface, mDividerPosition, 0.0f); t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height()); } else { - t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition); + t.setPosition(mDividerSurface, 0.0f, mDividerPosition); t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx); } - t.apply(); + if (mIsDragging) { + updateVeils(t); + } } + /** + * Updates the layout parameters of the layout used to host the divider. This method should + * be called only when {@link #mProperties} is changed. This should not be called while + * dragging, because the layout parameters are not changed during dragging. + */ private void updateLayout() { final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit @@ -552,12 +859,21 @@ class DividerPresenter { mViewHost.setView(mDividerLayout, lp); } - private void updateDivider() { + /** + * Updates the UI component of the divider, including the drag handle and the veils. This + * method should be called only when {@link #mProperties} is changed. This should not be + * called while dragging, because the UI components are not changed during dragging and + * only their surface positions are changed. + */ + private void updateDivider(@NonNull SurfaceControl.Transaction t) { mDividerLayout.removeAllViews(); mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb()); if (mProperties.mDividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + createVeils(); drawDragHandle(); + } else { + removeVeils(t); } mViewHost.getView().invalidate(); } @@ -580,7 +896,7 @@ class DividerPresenter { button.setLayoutParams(params); button.setBackgroundColor(R.color.transparent); - final Drawable handle = context.getResources().getDrawable( + final Drawable handle = context.getResources().getDrawable( R.drawable.activity_embedding_divider_handle, context.getTheme()); if (mProperties.mIsVerticalSplit) { button.setImageDrawable(handle); @@ -598,6 +914,8 @@ class DividerPresenter { button.setImageDrawable(rotatedHandle); } + + button.setOnTouchListener(mListener); mDividerLayout.addView(button); } @@ -613,5 +931,69 @@ class DividerPresenter { .setColorLayer() .build(); } + + private void createVeils() { + if (mPrimaryVeil == null) { + mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */); + } + if (mSecondaryVeil == null) { + mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */); + } + } + + private void removeVeils(@NonNull SurfaceControl.Transaction t) { + if (mPrimaryVeil != null) { + t.remove(mPrimaryVeil); + } + if (mSecondaryVeil != null) { + t.remove(mSecondaryVeil); + } + mPrimaryVeil = null; + mSecondaryVeil = null; + } + + private void showVeils(@NonNull SurfaceControl.Transaction t) { + t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR)) + .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR)) + .setLayer(mDividerSurface, DIVIDER_LAYER) + .setLayer(mPrimaryVeil, VEIL_LAYER) + .setLayer(mSecondaryVeil, VEIL_LAYER) + .setVisibility(mPrimaryVeil, true) + .setVisibility(mSecondaryVeil, true); + updateVeils(t); + } + + private void hideVeils(@NonNull SurfaceControl.Transaction t) { + t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false); + } + + private void updateVeils(@NonNull SurfaceControl.Transaction t) { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + + // Relative bounds of the primary and secondary containers in the Task. + Rect primaryBounds; + Rect secondaryBounds; + if (mProperties.mIsVerticalSplit) { + final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height()); + final Rect boundsRight = new Rect(mDividerPosition + mDividerWidthPx, 0, + taskBounds.width(), taskBounds.height()); + primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft; + secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight; + } else { + final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition); + final Rect boundsBottom = new Rect(0, mDividerPosition + mDividerWidthPx, + taskBounds.width(), taskBounds.height()); + primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop; + secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom; + } + t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height()); + t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height()); + t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top); + t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top); + } + + private static float[] colorToFloatArray(@NonNull Color color) { + return new float[]{color.red(), color.green(), color.blue()}; + } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 3f4dddf0cc81..32f2d67888ae 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -165,7 +165,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { /** * Expands an existing TaskFragment to fill parent. * @param wct WindowContainerTransaction in which the task fragment should be resized. - * @param fragmentToken token of an existing TaskFragment. + * @param container the {@link TaskFragmentContainer} to be expanded. */ void expandTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { @@ -174,8 +174,6 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { clearAdjacentTaskFragments(wct, fragmentToken); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); - - container.getTaskContainer().updateDivider(wct); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java new file mode 100644 index 000000000000..042a68a684c0 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import android.content.res.Configuration; +import android.view.View; + +import androidx.annotation.NonNull; + +/** Helper functions for {@link SplitAttributes} */ +class SplitAttributesHelper { + /** + * Returns whether the split layout direction is reversed. Right-to-left and bottom-to-top are + * considered reversed. + */ + static boolean isReversedLayout( + @NonNull SplitAttributes splitAttributes, @NonNull Configuration configuration) { + switch (splitAttributes.getLayoutDirection()) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + return false; + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return true; + case SplitAttributes.LayoutDirection.LOCALE: + return configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + default: + throw new IllegalArgumentException( + "Invalid layout direction:" + splitAttributes.getLayoutDirection()); + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 1bc8264d8e7e..b9b86f015606 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -110,7 +110,7 @@ import java.util.function.BiConsumer; * Main controller class that manages split states and presentation. */ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, - ActivityEmbeddingComponent { + ActivityEmbeddingComponent, DividerPresenter.DragEventCallback { static final String TAG = "SplitController"; static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @@ -163,6 +163,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>(); + /** Map from Task id to {@link DividerPresenter} which manages the divider in the Task. */ + @GuardedBy("mLock") + private final SparseArray<DividerPresenter> mDividerPresenters = new SparseArray<>(); + /** Callback to Jetpack to notify about changes to split states. */ @GuardedBy("mLock") @Nullable @@ -195,15 +199,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen : null; private final Handler mHandler; + private final MainThreadExecutor mExecutor; final Object mLock = new Object(); private final ActivityStartMonitor mActivityStartMonitor; public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent, @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { Log.i(TAG, "Initializing Activity Embedding Controller."); - final MainThreadExecutor executor = new MainThreadExecutor(); - mHandler = executor.mHandler; - mPresenter = new SplitPresenter(executor, windowLayoutComponent, this); + mExecutor = new MainThreadExecutor(); + mHandler = mExecutor.mHandler; + mPresenter = new SplitPresenter(mExecutor, windowLayoutComponent, this); mTransactionManager = new TransactionManager(mPresenter); final ActivityThread activityThread = ActivityThread.currentActivityThread(); final Application application = activityThread.getApplication(); @@ -844,7 +849,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Checks if container should be updated before apply new parentInfo. final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); - taskContainer.updateDivider(wct); + + // The divider need to be updated even if shouldUpdateContainer is false, because the decor + // surface may change in TaskFragmentParentInfo, which requires divider update but not + // container update. + updateDivider(wct, taskContainer); // If the last direct activity of the host task is dismissed and the overlay container is // the only taskFragment, the overlay container should also be dismissed. @@ -1007,6 +1016,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (taskContainer.isEmpty()) { // Cleanup the TaskContainer if it becomes empty. mTaskContainers.remove(taskContainer.getTaskId()); + mDividerPresenters.remove(taskContainer.getTaskId()); } return; } @@ -1759,6 +1769,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (!mTaskContainers.contains(taskId)) { mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask)); + mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor)); } final TaskContainer taskContainer = mTaskContainers.get(taskId); final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity, @@ -3065,4 +3076,46 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return configuration != null && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; } + + @GuardedBy("mLock") + void updateDivider( + @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) { + final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId()); + final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo(); + if (parentInfo != null) { + dividerPresenter.updateDivider( + wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer()); + } + } + + @Override + public void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action) { + synchronized (mLock) { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + action.accept(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } + + @Override + public void onFinishDragging( + int taskId, + @NonNull Consumer<WindowContainerTransaction> action) { + synchronized (mLock) { + final TransactionRecord transactionRecord = + mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + final TaskContainer taskContainer = mTaskContainers.get(taskId); + if (taskContainer != null) { + final DividerPresenter dividerPresenter = + mDividerPresenters.get(taskContainer.getTaskId()); + taskContainer.updateTopSplitContainerForDivider(dividerPresenter); + updateContainersInTask(wct, taskContainer); + } + action.accept(wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 20bc82002339..0d31266d771b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.content.pm.PackageManager.MATCH_ALL; import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; +import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import android.app.Activity; @@ -33,7 +34,6 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.util.LayoutDirection; import android.util.Pair; import android.util.Size; import android.view.View; @@ -368,7 +368,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); - taskContainer.updateDivider(wct); + mController.updateDivider(wct, taskContainer); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -697,6 +697,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { return RESULT_NOT_EXPANDED; } + /** + * Expands an existing TaskFragment to fill parent. + * @param wct WindowContainerTransaction in which the task fragment should be resized. + * @param container the {@link TaskFragmentContainer} to be expanded. + */ + void expandTaskFragment(@NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container) { + super.expandTaskFragment(wct, container); + mController.updateDivider(wct, container.getTaskContainer()); + } + static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { return shouldShowSplit(splitContainer.getCurrentSplitAttributes()); } @@ -1108,7 +1119,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { - final int layoutDirection = splitAttributes.getLayoutDirection(); final SplitType splitType = splitAttributes.getSplitType(); if (splitType instanceof ExpandContainersSplitType) { return splitType; @@ -1117,19 +1127,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary // computation have the same direction, which is from (top, left) to (bottom, right). final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); - switch (layoutDirection) { - case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: - case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: - return splitType; - case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: - case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: - return reversedSplitType; - case LayoutDirection.LOCALE: { - boolean isLtr = taskConfiguration.getLayoutDirection() - == View.LAYOUT_DIRECTION_LTR; - return isLtr ? splitType : reversedSplitType; - } - } + return isReversedLayout(splitAttributes, taskConfiguration) + ? reversedSplitType + : splitType; } else if (splitType instanceof HingeSplitType) { final HingeSplitType hinge = (HingeSplitType) splitType; @WindowingMode diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index e75a317cc3b3..a215bdf4b566 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -88,10 +88,6 @@ class TaskContainer { */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); - // TODO(b/293654166): move DividerPresenter to SplitController. - @NonNull - final DividerPresenter mDividerPresenter; - /** * The {@link TaskContainer} constructor * @@ -113,7 +109,6 @@ class TaskContainer { // the host task is visible and has an activity in the task. mIsVisible = true; mHasDirectActivity = true; - mDividerPresenter = new DividerPresenter(); } int getTaskId() { @@ -151,6 +146,11 @@ class TaskContainer { mTaskFragmentParentInfo = info; } + @Nullable + TaskFragmentParentInfo getTaskFragmentParentInfo() { + return mTaskFragmentParentInfo; + } + /** * Returns {@code true} if the container should be updated with {@code info}. */ @@ -398,16 +398,22 @@ class TaskContainer { return mContainers; } - void updateDivider(@NonNull WindowContainerTransaction wct) { - if (mTaskFragmentParentInfo != null) { - // Update divider only if TaskFragmentParentInfo is available. - mDividerPresenter.updateDivider( - wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer()); + void updateTopSplitContainerForDivider(@NonNull DividerPresenter dividerPresenter) { + final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer(); + if (topSplitContainer == null) { + return; } + + final float newRatio = dividerPresenter.calculateNewSplitRatio(topSplitContainer); + topSplitContainer.updateDefaultSplitAttributes( + new SplitAttributes.Builder(topSplitContainer.getDefaultSplitAttributes()) + .setSplitType(new SplitAttributes.SplitType.RatioSplitType(newRatio)) + .build() + ); } @Nullable - private SplitContainer getTopNonFinishingSplitContainer() { + SplitContainer getTopNonFinishingSplitContainer() { for (int i = mSplitContainers.size() - 1; i >= 0; i--) { final SplitContainer splitContainer = mSplitContainers.get(i); if (!splitContainer.getPrimaryContainer().isFinished() diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 4d1d807038eb..47d01da1c8b5 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -42,6 +42,7 @@ import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.view.Display; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.window.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; @@ -60,6 +61,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.concurrent.Executor; + /** * Test class for {@link DividerPresenter}. * @@ -73,6 +76,8 @@ public class DividerPresenterTest { @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + private static final int MOCK_TASK_ID = 1234; + @Mock private DividerPresenter.Renderer mRenderer; @@ -83,6 +88,12 @@ public class DividerPresenterTest { private TaskFragmentParentInfo mParentInfo; @Mock + private TaskContainer mTaskContainer; + + @Mock + private DividerPresenter.DragEventCallback mDragEventCallback; + + @Mock private SplitContainer mSplitContainer; @Mock @@ -110,6 +121,8 @@ public class DividerPresenterTest { MockitoAnnotations.initMocks(this); mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG); + when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID); + when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY); when(mParentInfo.getConfiguration()).thenReturn(new Configuration()); when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl); @@ -133,9 +146,11 @@ public class DividerPresenterTest { mSurfaceControl, getInitialDividerPosition(mSplitContainer), true /* isVerticalSplit */, + false /* isReversedLayout */, Display.DEFAULT_DISPLAY); - mDividerPresenter = new DividerPresenter(); + mDividerPresenter = new DividerPresenter( + MOCK_TASK_ID, mDragEventCallback, mock(Executor.class)); mDividerPresenter.mProperties = mProperties; mDividerPresenter.mRenderer = mRenderer; mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken; @@ -311,6 +326,184 @@ public class DividerPresenterTest { dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); } + @Test + public void testCalculateDividerPosition() { + final MotionEvent event = mock(MotionEvent.class); + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + when(event.getRawX()).thenReturn(500f); // Touch event is in display space + assertEquals( + // Touch position is in task space is 400, then minus half of divider width. + 375, + DividerPresenter.calculateDividerPosition( + event, + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Top-to-bottom split + when(event.getRawY()).thenReturn(1000f); // Touch event is in display space + assertEquals( + // Touch position is in task space is 800, then minus half of divider width. + 775, + DividerPresenter.calculateDividerPosition( + event, + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + false /* isReversedLayout */)); + } + + @Test + public void testCalculateMinPosition() { + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + assertEquals( + 255, /* (1000 - 100 - 50) * 0.3 */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Top-to-bottom split + assertEquals( + 525, /* (2000 - 200 - 50) * 0.3 */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Right-to-left split + assertEquals( + 170, /* (1000 - 100 - 50) * (1 - 0.8) */ + DividerPresenter.calculateMinPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + true /* isReversedLayout */)); + } + + @Test + public void testCalculateMaxPosition() { + final Rect taskBounds = new Rect(100, 200, 1000, 2000); + final int dividerWidthPx = 50; + final DividerAttributes dividerAttributes = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setPrimaryMinRatio(0.3f) + .setPrimaryMaxRatio(0.8f) + .build(); + + // Left-to-right split + assertEquals( + 680, /* (1000 - 100 - 50) * 0.8 */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Top-to-bottom split + assertEquals( + 1400, /* (2000 - 200 - 50) * 0.8 */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + false /* isVerticalSplit */, + false /* isReversedLayout */)); + + // Right-to-left split + assertEquals( + 595, /* (1000 - 100 - 50) * (1 - 0.3) */ + DividerPresenter.calculateMaxPosition( + taskBounds, + dividerWidthPx, + dividerAttributes, + true /* isVerticalSplit */, + true /* isReversedLayout */)); + } + + @Test + public void testCalculateNewSplitRatio_leftToRight() { + // primary=500px; secondary=500px; divider=100px; total=1100px. + final Rect taskBounds = new Rect(0, 0, 1100, 2000); + final Rect primaryBounds = new Rect(0, 0, 500, 2000); + final Rect secondaryBounds = new Rect(600, 0, 1100, 2000); + final int dividerWidthPx = 100; + final int dividerPosition = 300; + + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + assertEquals( + 0.3f, // Primary is 300px after dragging. + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + true /* isVerticalSplit */, + false /* isReversedLayout */), + 0.0001 /* delta */); + } + + @Test + public void testCalculateNewSplitRatio_bottomToTop() { + // Primary is at bottom. Secondary is at top. + // primary=500px; secondary=500px; divider=100px; total=1100px. + final Rect taskBounds = new Rect(0, 0, 2000, 1100); + final Rect primaryBounds = new Rect(0, 0, 2000, 1100); + final Rect secondaryBounds = new Rect(0, 0, 2000, 500); + final int dividerWidthPx = 100; + final int dividerPosition = 300; + + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + assertEquals( + // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100]. + 0.7f, + DividerPresenter.calculateNewSplitRatio( + mSplitContainer, + dividerPosition, + taskBounds, + dividerWidthPx, + false /* isVerticalSplit */, + true /* isReversedLayout */), + 0.0001 /* delta */); + } + private TaskFragmentContainer createMockTaskFragmentContainer( @NonNull IBinder token, @NonNull Rect bounds) { final TaskFragmentContainer container = mock(TaskFragmentContainer.class); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 014b8413bb10..cda94440affa 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -78,13 +78,13 @@ cc_defaults { include_dirs: [ "external/skia/include/private", "external/skia/src/core", + "external/skia/src/utils", ], target: { android: { include_dirs: [ "external/skia/src/image", - "external/skia/src/utils", "external/skia/src/gpu", "external/skia/src/shaders", ], @@ -345,6 +345,7 @@ cc_defaults { "jni/android_nio_utils.cpp", "jni/android_util_PathParser.cpp", + "jni/AnimatedImageDrawable.cpp", "jni/Bitmap.cpp", "jni/BitmapRegionDecoder.cpp", "jni/BufferUtils.cpp", @@ -418,7 +419,6 @@ cc_defaults { target: { android: { srcs: [ // sources that depend on android only libraries - "jni/AnimatedImageDrawable.cpp", "jni/android_graphics_TextureLayer.cpp", "jni/android_graphics_HardwareRenderer.cpp", "jni/android_graphics_HardwareBufferRenderer.cpp", @@ -529,7 +529,9 @@ cc_defaults { "effects/GainmapRenderer.cpp", "pipeline/skia/BackdropFilterDrawable.cpp", "pipeline/skia/HolePunch.cpp", + "pipeline/skia/SkiaCpuPipeline.cpp", "pipeline/skia/SkiaDisplayList.cpp", + "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaRecordingCanvas.cpp", "pipeline/skia/StretchMask.cpp", "pipeline/skia/RenderNodeDrawable.cpp", @@ -539,6 +541,7 @@ cc_defaults { "renderthread/RenderTask.cpp", "renderthread/TimeLord.cpp", "hwui/AnimatedImageDrawable.cpp", + "hwui/AnimatedImageThread.cpp", "hwui/Bitmap.cpp", "hwui/BlurDrawLooper.cpp", "hwui/Canvas.cpp", @@ -567,6 +570,7 @@ cc_defaults { "HWUIProperties.sysprop", "Interpolator.cpp", "JankTracker.cpp", + "LayerUpdateQueue.cpp", "LightingInfo.cpp", "Matrix.cpp", "Mesh.cpp", @@ -599,14 +603,13 @@ cc_defaults { local_include_dirs: ["platform/android"], srcs: [ - "hwui/AnimatedImageThread.cpp", "pipeline/skia/ATraceMemoryDump.cpp", "pipeline/skia/GLFunctorDrawable.cpp", "pipeline/skia/LayerDrawable.cpp", "pipeline/skia/ShaderCache.cpp", + "pipeline/skia/SkiaGpuPipeline.cpp", "pipeline/skia/SkiaMemoryTracer.cpp", "pipeline/skia/SkiaOpenGLPipeline.cpp", - "pipeline/skia/SkiaPipeline.cpp", "pipeline/skia/SkiaProfileRenderer.cpp", "pipeline/skia/SkiaVulkanPipeline.cpp", "pipeline/skia/VkFunctorDrawable.cpp", @@ -630,7 +633,6 @@ cc_defaults { "DeferredLayerUpdater.cpp", "HardwareBitmapUploader.cpp", "Layer.cpp", - "LayerUpdateQueue.cpp", "ProfileDataContainer.cpp", "Readback.cpp", "TreeInfo.cpp", diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index ec53070f6cb8..c1510d96461f 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -242,7 +242,7 @@ enum class ProfileType { None, Console, Bars }; enum class OverdrawColorSet { Default = 0, Deuteranomaly }; -enum class RenderPipelineType { SkiaGL, SkiaVulkan, NotInitialized = 128 }; +enum class RenderPipelineType { SkiaGL, SkiaVulkan, SkiaCpu, NotInitialized = 128 }; enum class StretchEffectBehavior { ShaderHWUI, // Stretch shader in HWUI only, matrix scale in SF diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 27773a60355a..69613c7d17cb 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -15,18 +15,16 @@ */ #include "AnimatedImageDrawable.h" -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread -#include "AnimatedImageThread.h" -#endif - -#include <gui/TraceUtils.h> -#include "pipeline/skia/SkiaUtils.h" #include <SkPicture.h> #include <SkRefCnt.h> +#include <gui/TraceUtils.h> #include <optional> +#include "AnimatedImageThread.h" +#include "pipeline/skia/SkiaUtils.h" + namespace android { AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed, @@ -185,10 +183,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { } else if (starting) { // The image has animated, and now is being reset. Queue up the first // frame, but keep showing the current frame until the first is ready. -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread auto& thread = uirenderer::AnimatedImageThread::getInstance(); mNextSnapshot = thread.reset(sk_ref_sp(this)); -#endif } bool finalFrame = false; @@ -214,10 +210,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) { } if (mRunning && !mNextSnapshot.valid()) { -#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread auto& thread = uirenderer::AnimatedImageThread::getInstance(); mNextSnapshot = thread.decodeNextFrame(sk_ref_sp(this)); -#endif } if (!drawDirectly) { diff --git a/libs/hwui/hwui/AnimatedImageThread.cpp b/libs/hwui/hwui/AnimatedImageThread.cpp index 825dd4cf2bf1..e39c8d57d31c 100644 --- a/libs/hwui/hwui/AnimatedImageThread.cpp +++ b/libs/hwui/hwui/AnimatedImageThread.cpp @@ -16,7 +16,9 @@ #include "AnimatedImageThread.h" +#ifdef __ANDROID__ #include <sys/resource.h> +#endif namespace android { namespace uirenderer { @@ -31,7 +33,9 @@ AnimatedImageThread& AnimatedImageThread::getInstance() { } AnimatedImageThread::AnimatedImageThread() { +#ifdef __ANDROID__ setpriority(PRIO_PROCESS, 0, PRIORITY_NORMAL + PRIORITY_MORE_FAVORABLE); +#endif } std::future<AnimatedImageDrawable::Snapshot> AnimatedImageThread::decodeNextFrame( diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index 90b1da846205..0f80c55d0ed0 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -25,7 +25,9 @@ #include <hwui/AnimatedImageDrawable.h> #include <hwui/Canvas.h> #include <hwui/ImageDecoder.h> +#ifdef __ANDROID__ #include <utils/Looper.h> +#endif #include "ColorFilter.h" #include "GraphicsJNI.h" @@ -180,6 +182,23 @@ static void AnimatedImageDrawable_nSetRepeatCount(JNIEnv* env, jobject /*clazz*/ drawable->setRepetitionCount(loopCount); } +#ifndef __ANDROID__ +struct Message { + Message(int w) {} +}; + +class MessageHandler : public virtual RefBase { +protected: + virtual ~MessageHandler() override {} + +public: + /** + * Handles a message. + */ + virtual void handleMessage(const Message& message) = 0; +}; +#endif + class InvokeListener : public MessageHandler { public: InvokeListener(JNIEnv* env, jobject javaObject) { @@ -204,6 +223,7 @@ private: }; class JniAnimationEndListener : public OnAnimationEndListener { +#ifdef __ANDROID__ public: JniAnimationEndListener(sp<Looper>&& looper, JNIEnv* env, jobject javaObject) { mListener = new InvokeListener(env, javaObject); @@ -215,6 +235,17 @@ public: private: sp<InvokeListener> mListener; sp<Looper> mLooper; +#else +public: + JniAnimationEndListener(JNIEnv* env, jobject javaObject) { + mListener = new InvokeListener(env, javaObject); + } + + void onAnimationEnd() override { mListener->handleMessage(0); } + +private: + sp<InvokeListener> mListener; +#endif }; static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobject /*clazz*/, @@ -223,6 +254,7 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec if (!jdrawable) { drawable->setOnAnimationEndListener(nullptr); } else { +#ifdef __ANDROID__ sp<Looper> looper = Looper::getForThread(); if (!looper.get()) { doThrowISE(env, @@ -233,6 +265,10 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec drawable->setOnAnimationEndListener( std::make_unique<JniAnimationEndListener>(std::move(looper), env, jdrawable)); +#else + drawable->setOnAnimationEndListener( + std::make_unique<JniAnimationEndListener>(env, jdrawable)); +#endif } } diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp new file mode 100644 index 000000000000..9b8373cea66d --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp @@ -0,0 +1,122 @@ +/* + * 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. + */ + +#include "pipeline/skia/SkiaCpuPipeline.h" + +#include <system/window.h> + +#include "DeviceInfo.h" +#include "LightingInfo.h" +#include "renderthread/Frame.h" +#include "utils/Color.h" + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +void SkiaCpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + renderLayerImpl(layers.entries()[i].renderNode.get(), layers.entries()[i].damage); + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaCpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + node->setLayerSurface(SkSurfaces::Raster(info, &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +MakeCurrentResult SkiaCpuPipeline::makeCurrent() { + return MakeCurrentResult::AlreadyCurrent; +} + +Frame SkiaCpuPipeline::getFrame() { + return Frame(mSurface->width(), mSurface->height(), 0); +} + +IRenderPipeline::DrawResult SkiaCpuPipeline::draw( + const Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) { + LightingInfo::updateLighting(lightGeometry, lightInfo); + renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mSurface, + SkMatrix::I()); + return {true, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd{}}; +} + +bool SkiaCpuPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) { + if (surface) { + ANativeWindowBuffer* buffer; + surface->dequeueBuffer(surface, &buffer, nullptr); + int width, height; + surface->query(surface, NATIVE_WINDOW_WIDTH, &width); + surface->query(surface, NATIVE_WINDOW_HEIGHT, &height); + SkImageInfo imageInfo = + SkImageInfo::Make(width, height, mSurfaceColorType, + SkAlphaType::kPremul_SkAlphaType, mSurfaceColorSpace); + size_t widthBytes = width * imageInfo.bytesPerPixel(); + void* pixels = buffer->reserved[0]; + mSurface = SkSurfaces::WrapPixels(imageInfo, pixels, widthBytes); + } else { + mSurface = sk_sp<SkSurface>(); + } + return true; +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.h b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h new file mode 100644 index 000000000000..5a1014c2c2de --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h @@ -0,0 +1,77 @@ +/* + * 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaCpuPipeline : public SkiaPipeline { +public: + SkiaCpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaCpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override; + renderthread::Frame getFrame() override; + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override; + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override; + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return mSurface.get() != nullptr; } + bool isContextReady() override { return true; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + +private: + sk_sp<SkSurface> mSurface; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp new file mode 100644 index 000000000000..cd9daf437bdb --- /dev/null +++ b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016 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. + */ + +#include "pipeline/skia/SkiaGpuPipeline.h" + +#include <SkImageAndroid.h> +#include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> + +using namespace android::uirenderer::renderthread; + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +SkiaGpuPipeline::SkiaGpuPipeline(RenderThread& thread) : SkiaPipeline(thread) {} + +SkiaGpuPipeline::~SkiaGpuPipeline() { + unpinImages(); +} + +void SkiaGpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { + sk_sp<GrDirectContext> cachedContext; + + // Render all layers that need to be updated, in order. + for (size_t i = 0; i < layers.entries().size(); i++) { + RenderNode* layerNode = layers.entries()[i].renderNode.get(); + renderLayerImpl(layerNode, layers.entries()[i].damage); + // cache the current context so that we can defer flushing it until + // either all the layers have been rendered or the context changes + GrDirectContext* currentContext = + GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); + if (cachedContext.get() != currentContext) { + if (cachedContext.get()) { + ATRACE_NAME("flush layers (context changed)"); + cachedContext->flushAndSubmit(); + } + cachedContext.reset(SkSafeRef(currentContext)); + } + } + if (cachedContext.get()) { + ATRACE_NAME("flush layers"); + cachedContext->flushAndSubmit(); + } +} + +// If the given node didn't have a layer surface, or had one of the wrong size, this method +// creates a new one and returns true. Otherwise does nothing and returns false. +bool SkiaGpuPipeline::createOrUpdateLayer(RenderNode* node, + const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) { + // compute the size of the surface (i.e. texture) to be allocated for this layer + const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; + const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; + + SkSurface* layer = node->getLayerSurface(); + if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { + SkImageInfo info; + info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), + kPremul_SkAlphaType, getSurfaceColorSpace()); + SkSurfaceProps props(0, kUnknown_SkPixelGeometry); + SkASSERT(mRenderThread.getGrContext() != nullptr); + node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, info, 0, + this->getSurfaceOrigin(), &props)); + if (node->getLayerSurface()) { + // update the transform in window of the layer to reset its origin wrt light source + // position + Matrix4 windowTransform; + damageAccumulator.computeCurrentTransform(&windowTransform); + node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); + } else { + String8 cachesOutput; + mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, + &mRenderThread.renderState()); + ALOGE("%s", cachesOutput.c_str()); + if (errorHandler) { + std::ostringstream err; + err << "Unable to create layer for " << node->getName(); + const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); + err << ", size " << info.width() << "x" << info.height() << " max size " + << maxTextureSize << " color type " << (int)info.colorType() << " has context " + << (int)(mRenderThread.getGrContext() != nullptr); + errorHandler->onError(err.str()); + } + } + return true; + } + return false; +} + +bool SkiaGpuPipeline::pinImages(std::vector<SkImage*>& mutableImages) { + if (!mRenderThread.getGrContext()) { + ALOGD("Trying to pin an image with an invalid GrContext"); + return false; + } + for (SkImage* image : mutableImages) { + if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { + mPinnedImages.emplace_back(sk_ref_sp(image)); + } else { + return false; + } + } + return true; +} + +void SkiaGpuPipeline::unpinImages() { + for (auto& image : mPinnedImages) { + skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); + } + mPinnedImages.clear(); +} + +void SkiaGpuPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { + GrDirectContext* context = thread.getGrContext(); + if (context && !bitmap->isHardware()) { + ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); + auto image = bitmap->makeImage(); + if (image.get()) { + skgpu::ganesh::PinAsTexture(context, image.get()); + skgpu::ganesh::UnpinTexture(context, image.get()); + // A submit is necessary as there may not be a frame coming soon, so without a call + // to submit these texture uploads can just sit in the queue building up until + // we run out of RAM + context->flushAndSubmit(); + } + } +} + +sk_sp<SkSurface> SkiaGpuPipeline::getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams) { + auto bufferColorSpace = bufferParams.getColorSpace(); + if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || + !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { + mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( + mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, + bufferColorSpace, nullptr, true); + mBufferColorSpace = bufferColorSpace; + } + return mBufferSurface; +} + +void SkiaGpuPipeline::dumpResourceCacheUsage() const { + int resources; + size_t bytes; + mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); + size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); + + SkString log("Resource Cache Usage:\n"); + log.appendf("%8d items\n", resources); + log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, + bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); + + ALOGD("%s", log.c_str()); +} + +void SkiaGpuPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { + if (mHardwareBuffer) { + AHardwareBuffer_release(mHardwareBuffer); + mHardwareBuffer = nullptr; + } + + if (buffer) { + AHardwareBuffer_acquire(buffer); + mHardwareBuffer = buffer; + } +} + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp index c8d598702a7c..e4b1f916b4d6 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp @@ -14,25 +14,25 @@ * limitations under the License. */ -#include "SkiaOpenGLPipeline.h" +#include "pipeline/skia/SkiaOpenGLPipeline.h" -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> -#include <include/gpu/gl/GrGLTypes.h> #include <GrBackendSurface.h> #include <SkBlendMode.h> #include <SkImageInfo.h> #include <cutils/properties.h> #include <gui/TraceUtils.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> +#include <include/gpu/ganesh/gl/GrGLBackendSurface.h> +#include <include/gpu/gl/GrGLTypes.h> #include <strings.h> #include "DeferredLayerUpdater.h" #include "FrameInfo.h" -#include "LayerDrawable.h" #include "LightingInfo.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" #include "hwui/Bitmap.h" +#include "pipeline/skia/LayerDrawable.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" #include "private/hwui/DrawGlInfo.h" #include "renderstate/RenderState.h" #include "renderthread/EglManager.h" @@ -47,7 +47,7 @@ namespace uirenderer { namespace skiapipeline { SkiaOpenGLPipeline::SkiaOpenGLPipeline(RenderThread& thread) - : SkiaPipeline(thread), mEglManager(thread.eglManager()) { + : SkiaGpuPipeline(thread), mEglManager(thread.eglManager()) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 99469d1e3628..2cfdd3fb0315 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -14,11 +14,8 @@ * limitations under the License. */ -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaPipeline.h" -#include <include/android/SkSurfaceAndroid.h> -#include <include/gpu/ganesh/SkSurfaceGanesh.h> -#include <include/encode/SkPngEncoder.h> #include <SkCanvas.h> #include <SkColor.h> #include <SkColorSpace.h> @@ -40,6 +37,9 @@ #include <SkTypeface.h> #include <android-base/properties.h> #include <gui/TraceUtils.h> +#include <include/android/SkSurfaceAndroid.h> +#include <include/encode/SkPngEncoder.h> +#include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <unistd.h> #include <sstream> @@ -62,37 +62,13 @@ SkiaPipeline::SkiaPipeline(RenderThread& thread) : mRenderThread(thread) { setSurfaceColorProperties(mColorMode); } -SkiaPipeline::~SkiaPipeline() { - unpinImages(); -} +SkiaPipeline::~SkiaPipeline() {} void SkiaPipeline::onDestroyHardwareResources() { unpinImages(); mRenderThread.cacheManager().trimStaleResources(); } -bool SkiaPipeline::pinImages(std::vector<SkImage*>& mutableImages) { - if (!mRenderThread.getGrContext()) { - ALOGD("Trying to pin an image with an invalid GrContext"); - return false; - } - for (SkImage* image : mutableImages) { - if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) { - mPinnedImages.emplace_back(sk_ref_sp(image)); - } else { - return false; - } - } - return true; -} - -void SkiaPipeline::unpinImages() { - for (auto& image : mPinnedImages) { - skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get()); - } - mPinnedImages.clear(); -} - void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) { @@ -102,136 +78,53 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry, layerUpdateQueue->clear(); } -void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) { - sk_sp<GrDirectContext> cachedContext; - - // Render all layers that need to be updated, in order. - for (size_t i = 0; i < layers.entries().size(); i++) { - RenderNode* layerNode = layers.entries()[i].renderNode.get(); - // only schedule repaint if node still on layer - possible it may have been - // removed during a dropped frame, but layers may still remain scheduled so - // as not to lose info on what portion is damaged - if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { - continue; - } - SkASSERT(layerNode->getLayerSurface()); - SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); - if (!displayList || displayList->isEmpty()) { - ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); - return; - } - - const Rect& layerDamage = layers.entries()[i].damage; - - SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - - int saveCount = layerCanvas->save(); - SkASSERT(saveCount == 1); - - layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - - // TODO: put localized light center calculation and storage to a drawable related code. - // It does not seem right to store something localized in a global state - // fix here and in recordLayers - const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); - Vector3 transformedLightCenter(savedLightCenter); - // map current light center into RenderNode's coordinate space - layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); - LightingInfo::setLightCenterRaw(transformedLightCenter); - - const RenderProperties& properties = layerNode->properties(); - const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); - if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { - return; - } - - ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), - bounds.height()); +void SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) { + // only schedule repaint if node still on layer - possible it may have been + // removed during a dropped frame, but layers may still remain scheduled so + // as not to lose info on what portion is damaged + if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) { + return; + } + SkASSERT(layerNode->getLayerSurface()); + SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl(); + if (!displayList || displayList->isEmpty()) { + ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName()); + return; + } - layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; - layerCanvas->clear(SK_ColorTRANSPARENT); + SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas(); - RenderNodeDrawable root(layerNode, layerCanvas, false); - root.forceDraw(layerCanvas); - layerCanvas->restoreToCount(saveCount); + int saveCount = layerCanvas->save(); + SkASSERT(saveCount == 1); - LightingInfo::setLightCenterRaw(savedLightCenter); + layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect()); - // cache the current context so that we can defer flushing it until - // either all the layers have been rendered or the context changes - GrDirectContext* currentContext = - GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext()); - if (cachedContext.get() != currentContext) { - if (cachedContext.get()) { - ATRACE_NAME("flush layers (context changed)"); - cachedContext->flushAndSubmit(); - } - cachedContext.reset(SkSafeRef(currentContext)); - } + // TODO: put localized light center calculation and storage to a drawable related code. + // It does not seem right to store something localized in a global state + // fix here and in recordLayers + const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw()); + Vector3 transformedLightCenter(savedLightCenter); + // map current light center into RenderNode's coordinate space + layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter); + LightingInfo::setLightCenterRaw(transformedLightCenter); + + const RenderProperties& properties = layerNode->properties(); + const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight()); + if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) { + return; } - if (cachedContext.get()) { - ATRACE_NAME("flush layers"); - cachedContext->flushAndSubmit(); - } -} + ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(), + bounds.height()); -bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) { - // compute the size of the surface (i.e. texture) to be allocated for this layer - const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE; - const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE; - - SkSurface* layer = node->getLayerSurface(); - if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) { - SkImageInfo info; - info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(), - kPremul_SkAlphaType, getSurfaceColorSpace()); - SkSurfaceProps props(0, kUnknown_SkPixelGeometry); - SkASSERT(mRenderThread.getGrContext() != nullptr); - node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(), - skgpu::Budgeted::kYes, info, 0, - this->getSurfaceOrigin(), &props)); - if (node->getLayerSurface()) { - // update the transform in window of the layer to reset its origin wrt light source - // position - Matrix4 windowTransform; - damageAccumulator.computeCurrentTransform(&windowTransform); - node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform); - } else { - String8 cachesOutput; - mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput, - &mRenderThread.renderState()); - ALOGE("%s", cachesOutput.c_str()); - if (errorHandler) { - std::ostringstream err; - err << "Unable to create layer for " << node->getName(); - const int maxTextureSize = DeviceInfo::get()->maxTextureSize(); - err << ", size " << info.width() << "x" << info.height() << " max size " - << maxTextureSize << " color type " << (int)info.colorType() << " has context " - << (int)(mRenderThread.getGrContext() != nullptr); - errorHandler->onError(err.str()); - } - } - return true; - } - return false; -} + layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false; + layerCanvas->clear(SK_ColorTRANSPARENT); -void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - GrDirectContext* context = thread.getGrContext(); - if (context && !bitmap->isHardware()) { - ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height()); - auto image = bitmap->makeImage(); - if (image.get()) { - skgpu::ganesh::PinAsTexture(context, image.get()); - skgpu::ganesh::UnpinTexture(context, image.get()); - // A submit is necessary as there may not be a frame coming soon, so without a call - // to submit these texture uploads can just sit in the queue building up until - // we run out of RAM - context->flushAndSubmit(); - } - } + RenderNodeDrawable root(layerNode, layerCanvas, false); + root.forceDraw(layerCanvas); + layerCanvas->restoreToCount(saveCount); + + LightingInfo::setLightCenterRaw(savedLightCenter); } static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) { @@ -599,45 +492,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip, } } -void SkiaPipeline::dumpResourceCacheUsage() const { - int resources; - size_t bytes; - mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes); - size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit(); - - SkString log("Resource Cache Usage:\n"); - log.appendf("%8d items\n", resources); - log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes, - bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f))); - - ALOGD("%s", log.c_str()); -} - -void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) { - if (mHardwareBuffer) { - AHardwareBuffer_release(mHardwareBuffer); - mHardwareBuffer = nullptr; - } - - if (buffer) { - AHardwareBuffer_acquire(buffer); - mHardwareBuffer = buffer; - } -} - -sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams) { - auto bufferColorSpace = bufferParams.getColorSpace(); - if (mBufferSurface == nullptr || mBufferColorSpace == nullptr || - !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) { - mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer( - mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin, - bufferColorSpace, nullptr, true); - mBufferColorSpace = bufferColorSpace; - } - return mBufferSurface; -} - void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mColorMode = colorMode; switch (colorMode) { diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index befee8989383..f9d37b924321 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -42,18 +42,9 @@ public: void onDestroyHardwareResources() override; - bool pinImages(std::vector<SkImage*>& mutableImages) override; - bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } - void unpinImages() override; - void renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, bool opaque, const LightInfo& lightInfo) override; - // If the given node didn't have a layer surface, or had one of the wrong size, this method - // creates a new one and returns true. Otherwise does nothing and returns false. - bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, - ErrorHandler* errorHandler) override; - void setSurfaceColorProperties(ColorMode colorMode) override; SkColorType getSurfaceColorType() const override { return mSurfaceColorType; } sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; } @@ -63,9 +54,8 @@ public: const Rect& contentDrawBounds, sk_sp<SkSurface> surface, const SkMatrix& preTransform); - static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); - - void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque); + void renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage); + virtual void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) = 0; // Sets the recording callback to the provided function and the recording mode // to CallbackAPI @@ -75,19 +65,11 @@ public: mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None; } - virtual void setHardwareBuffer(AHardwareBuffer* buffer) override; - bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } - void setTargetSdrHdrRatio(float ratio) override; protected: - sk_sp<SkSurface> getBufferSkSurface( - const renderthread::HardwareBufferRenderParams& bufferParams); - void dumpResourceCacheUsage() const; - renderthread::RenderThread& mRenderThread; - AHardwareBuffer* mHardwareBuffer = nullptr; sk_sp<SkSurface> mBufferSurface = nullptr; sk_sp<SkColorSpace> mBufferColorSpace = nullptr; @@ -125,8 +107,6 @@ private: // Set up a multi frame capture. bool setupMultiFrameCapture(); - std::vector<sk_sp<SkImage>> mPinnedImages; - // Block of properties used only for debugging to record a SkPicture and save it in a file. // There are three possible ways of recording drawing commands. enum class CaptureMode { diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp index fd0a8e06f39c..d06dba05ee88 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "SkiaVulkanPipeline.h" +#include "pipeline/skia/SkiaVulkanPipeline.h" #include <GrDirectContext.h> #include <GrTypes.h> @@ -28,10 +28,10 @@ #include "DeferredLayerUpdater.h" #include "LightingInfo.h" #include "Readback.h" -#include "ShaderCache.h" -#include "SkiaPipeline.h" -#include "SkiaProfileRenderer.h" -#include "VkInteropFunctorDrawable.h" +#include "pipeline/skia/ShaderCache.h" +#include "pipeline/skia/SkiaGpuPipeline.h" +#include "pipeline/skia/SkiaProfileRenderer.h" +#include "pipeline/skia/VkInteropFunctorDrawable.h" #include "renderstate/RenderState.h" #include "renderthread/Frame.h" #include "renderthread/IRenderPipeline.h" @@ -42,7 +42,8 @@ namespace android { namespace uirenderer { namespace skiapipeline { -SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) { +SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) + : SkiaGpuPipeline(thread) { thread.renderState().registerContextCallback(this); } diff --git a/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..9159eae46065 --- /dev/null +++ b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread); + virtual ~SkiaGpuPipeline(); + + virtual GrSurfaceOrigin getSurfaceOrigin() = 0; + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override; + + bool pinImages(std::vector<SkImage*>& mutableImages) override; + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override; + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override; + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override; + bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; } + + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap); + +protected: + sk_sp<SkSurface> getBufferSkSurface( + const renderthread::HardwareBufferRenderParams& bufferParams); + void dumpResourceCacheUsage() const; + + AHardwareBuffer* mHardwareBuffer = nullptr; + +private: + std::vector<sk_sp<SkImage>> mPinnedImages; +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h index ebe8b6e15d44..6e7478288777 100644 --- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h @@ -19,7 +19,7 @@ #include <EGL/egl.h> #include <system/window.h> -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" @@ -30,7 +30,7 @@ class Bitmap; namespace uirenderer { namespace skiapipeline { -class SkiaOpenGLPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaOpenGLPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: SkiaOpenGLPipeline(renderthread::RenderThread& thread); virtual ~SkiaOpenGLPipeline(); diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h index 624eaa51a584..0d30df48baee 100644 --- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h +++ b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h @@ -17,7 +17,7 @@ #pragma once #include "SkRefCnt.h" -#include "SkiaPipeline.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "renderstate/RenderState.h" #include "renderthread/HardwareBufferRenderParams.h" #include "renderthread/VulkanManager.h" @@ -30,7 +30,7 @@ namespace android { namespace uirenderer { namespace skiapipeline { -class SkiaVulkanPipeline : public SkiaPipeline, public IGpuContextCallback { +class SkiaVulkanPipeline : public SkiaGpuPipeline, public IGpuContextCallback { public: explicit SkiaVulkanPipeline(renderthread::RenderThread& thread); virtual ~SkiaVulkanPipeline(); diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h new file mode 120000 index 000000000000..4fb4784f9f60 --- /dev/null +++ b/libs/hwui/platform/host/android/api-level.h @@ -0,0 +1 @@ +../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h new file mode 100644 index 000000000000..a71726585081 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaPipeline.h" +#include "renderthread/Frame.h" + +namespace android { +namespace uirenderer { +namespace skiapipeline { + +class SkiaGpuPipeline : public SkiaPipeline { +public: + SkiaGpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {} + ~SkiaGpuPipeline() {} + + bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; } + bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; } + void unpinImages() override {} + + // If the given node didn't have a layer surface, or had one of the wrong size, this method + // creates a new one and returns true. Otherwise does nothing and returns false. + bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator, + ErrorHandler* errorHandler) override { + return false; + } + void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override {} + void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {} + bool hasHardwareBuffer() override { return false; } + + renderthread::MakeCurrentResult makeCurrent() override { + return renderthread::MakeCurrentResult::Failed; + } + renderthread::Frame getFrame() override { return renderthread::Frame(0, 0, 0); } + renderthread::IRenderPipeline::DrawResult draw( + const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty, + const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue, + const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo, + const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler, + const renderthread::HardwareBufferRenderParams& bufferParams, + std::mutex& profilerLock) override { + return {false, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd(-1)}; + } + bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult, + const SkRect& screenDirty, FrameInfo* currentFrameInfo, + bool* requireSwap) override { + return false; + } + DeferredLayerUpdater* createTextureLayer() override { return nullptr; } + bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override { + return false; + } + [[nodiscard]] android::base::unique_fd flush() override { + return android::base::unique_fd(-1); + }; + void onStop() override {} + bool isSurfaceReady() override { return false; } + bool isContextReady() override { return false; } + + const SkM44& getPixelSnapMatrix() const override { + static const SkM44 sSnapMatrix = SkM44(); + return sSnapMatrix; + } + static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h new file mode 100644 index 000000000000..4fafbcc4748d --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaOpenGLPipeline : public SkiaGpuPipeline { +public: + SkiaOpenGLPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h new file mode 100644 index 000000000000..d54caef45bb5 --- /dev/null +++ b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 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. + */ + +#pragma once + +#include "pipeline/skia/SkiaGpuPipeline.h" + +namespace android { + +namespace uirenderer { +namespace skiapipeline { + +class SkiaVulkanPipeline : public SkiaGpuPipeline { +public: + SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {} + + static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {} +}; + +} /* namespace skiapipeline */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index abf64d099935..984916cb3986 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -35,8 +35,8 @@ #include "Properties.h" #include "RenderThread.h" #include "hwui/Canvas.h" +#include "pipeline/skia/SkiaGpuPipeline.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" -#include "pipeline/skia/SkiaPipeline.h" #include "pipeline/skia/SkiaVulkanPipeline.h" #include "thread/CommonPool.h" #include "utils/GLUtils.h" @@ -108,7 +108,7 @@ void CanvasContext::invokeFunctor(const RenderThread& thread, Functor* functor) } void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) { - skiapipeline::SkiaPipeline::prepareToDraw(thread, bitmap); + skiapipeline::SkiaGpuPipeline::prepareToDraw(thread, bitmap); } CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h index b8c3a4de2bd4..ee1d1f8789d9 100644 --- a/libs/hwui/renderthread/IRenderPipeline.h +++ b/libs/hwui/renderthread/IRenderPipeline.h @@ -30,8 +30,6 @@ #include "SwapBehavior.h" #include "hwui/Bitmap.h" -class GrDirectContext; - struct ANativeWindow; namespace android { @@ -94,7 +92,6 @@ public: virtual void setSurfaceColorProperties(ColorMode colorMode) = 0; virtual SkColorType getSurfaceColorType() const = 0; virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0; - virtual GrSurfaceOrigin getSurfaceOrigin() = 0; virtual void setPictureCapturedCallback( const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0; diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 7727078a42ec..63cb94516739 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -50,7 +50,7 @@ interface IMediaRouterService { // MediaRouterService.java for readability. // Methods for MediaRouter2 - List<MediaRoute2Info> getSystemRoutes(); + List<MediaRoute2Info> getSystemRoutes(String callerPackageName, boolean isProxyRouter); RoutingSessionInfo getSystemSessionInfo(); void registerRouter2(IMediaRouter2 router, String packageName); @@ -75,7 +75,8 @@ interface IMediaRouterService { // Methods for MediaRouter2Manager List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager); - RoutingSessionInfo getSystemSessionInfoForPackage(String packageName); + RoutingSessionInfo getSystemSessionInfoForPackage(String callerPackageName, + String targetPackageName); void registerManager(IMediaRouter2Manager manager, String packageName); void registerProxyRouter(IMediaRouter2Manager manager, String callingPackageName, String targetPackageName, in UserHandle targetUser); void unregisterManager(IMediaRouter2Manager manager); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index e0258ba4f61d..3f9440b60202 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -613,7 +613,7 @@ public final class MediaRouter2 { mImpl = new LocalMediaRouter2Impl(mContext.getPackageName()); mHandler = new Handler(Looper.getMainLooper()); - loadSystemRoutes(); + loadSystemRoutes(/* isProxyRouter */ false); RoutingSessionInfo currentSystemSessionInfo = mImpl.getSystemSessionInfo(); if (currentSystemSessionInfo == null) { @@ -631,21 +631,22 @@ public final class MediaRouter2 { IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); - loadSystemRoutes(); + loadSystemRoutes(/* isProxyRouter */ true); mSystemController = new SystemRoutingController( ProxyMediaRouter2Impl.getSystemSessionInfoImpl( - mMediaRouterService, clientPackageName)); + mMediaRouterService, mContext.getPackageName(), clientPackageName)); mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user); } @GuardedBy("mLock") - private void loadSystemRoutes() { + private void loadSystemRoutes(boolean isProxyRouter) { List<MediaRoute2Info> currentSystemRoutes = null; try { - currentSystemRoutes = mMediaRouterService.getSystemRoutes(); + currentSystemRoutes = mMediaRouterService.getSystemRoutes(mContext.getPackageName(), + isProxyRouter); } catch (RemoteException ex) { ex.rethrowFromSystemServer(); } @@ -2644,7 +2645,8 @@ public final class MediaRouter2 { @Override public RoutingSessionInfo getSystemSessionInfo() { - return getSystemSessionInfoImpl(mMediaRouterService, mClientPackageName); + return getSystemSessionInfoImpl( + mMediaRouterService, mContext.getPackageName(), mClientPackageName); } /** @@ -3049,9 +3051,11 @@ public final class MediaRouter2 { * <p>Extracted into a static method to allow calling this from the constructor. */ /* package */ static RoutingSessionInfo getSystemSessionInfoImpl( - @NonNull IMediaRouterService service, @NonNull String clientPackageName) { + @NonNull IMediaRouterService service, + @NonNull String callerPackageName, + @NonNull String clientPackageName) { try { - return service.getSystemSessionInfoForPackage(clientPackageName); + return service.getSystemSessionInfoForPackage(callerPackageName, clientPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 7756d93a5c38..e62d112a7ccb 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -81,6 +81,7 @@ public final class MediaRouter2Manager { @GuardedBy("sLock") private static MediaRouter2Manager sInstance; + private final Context mContext; private final MediaSessionManager mMediaSessionManager; private final Client mClient; private final IMediaRouterService mMediaRouterService; @@ -120,6 +121,7 @@ public final class MediaRouter2Manager { } private MediaRouter2Manager(Context context) { + mContext = context.getApplicationContext(); mMediaRouterService = IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); mMediaSessionManager = (MediaSessionManager) context @@ -374,16 +376,17 @@ public final class MediaRouter2Manager { } /** - * Gets the system routing session for the given {@code packageName}. - * Apps can select a route that is not the global route. (e.g. an app can select the device - * route while BT route is available.) + * Gets the system routing session for the given {@code targetPackageName}. Apps can select a + * route that is not the global route. (e.g. an app can select the device route while BT route + * is available.) * - * @param packageName the package name of the application. + * @param targetPackageName the package name of the application. */ @Nullable - public RoutingSessionInfo getSystemRoutingSession(@Nullable String packageName) { + public RoutingSessionInfo getSystemRoutingSession(@Nullable String targetPackageName) { try { - return mMediaRouterService.getSystemSessionInfoForPackage(packageName); + return mMediaRouterService.getSystemSessionInfoForPackage( + mContext.getPackageName(), targetPackageName); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 80b2be2567a7..6d4cc3a9ca44 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -194,13 +194,13 @@ package android.nfc { package android.nfc.cardemulation { public final class CardEmulation { - method @Deprecated public boolean categoryAllowsForegroundPreference(String); + method public boolean categoryAllowsForegroundPreference(String); method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public java.util.List<java.lang.String> getAidsForPreferredPaymentService(); method public java.util.List<java.lang.String> getAidsForService(android.content.ComponentName, String); - method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService(); + method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public CharSequence getDescriptionForPreferredPaymentService(); method public static android.nfc.cardemulation.CardEmulation getInstance(android.nfc.NfcAdapter); - method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService(); - method @Deprecated public int getSelectionModeForCategory(String); + method @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public String getRouteDestinationForPreferredPaymentService(); + method public int getSelectionModeForCategory(String); method public boolean isDefaultServiceForAid(android.content.ComponentName, String); method public boolean isDefaultServiceForCategory(android.content.ComponentName, String); method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>); diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 67697a429a32..de9eada18104 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -36,6 +36,7 @@ import android.nfc.Constants; import android.nfc.Flags; import android.nfc.INfcCardEmulation; import android.nfc.NfcAdapter; +import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; @@ -271,30 +272,31 @@ public final class CardEmulation { } /** + * <p> * Returns whether the user has allowed AIDs registered in the * specified category to be handled by a service that is preferred * by the foreground application, instead of by a pre-configured default. * * Foreground applications can set such preferences using the * {@link #setPreferredService(Activity, ComponentName)} method. + * <p class="note"> + * Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, this method will always + * return true. * * @param category The category, e.g. {@link #CATEGORY_PAYMENT} * @return whether AIDs in the category can be handled by a service * specified by the foreground app. - * - * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the - * Preferred Payment service is no longer valid. All routings will be done in a AID - * category agnostic manner. */ @SuppressWarnings("NonUserGetterCalled") - @Deprecated public boolean categoryAllowsForegroundPreference(String category) { Context contextAsUser = mContext.createContextAsUser( UserHandle.of(UserHandle.myUserId()), 0); + RoleManager roleManager = contextAsUser.getSystemService(RoleManager.class); if (roleManager.isRoleAvailable(RoleManager.ROLE_WALLET)) { return true; } + if (CATEGORY_PAYMENT.equals(category)) { boolean preferForeground = false; try { @@ -319,14 +321,14 @@ public final class CardEmulation { * every time what service they would like to use in this category. * <p>{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked * to pick a service if there is a conflict. + * + * <p class="note"> + * Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the default service defined + * by the holder of {@link android.app.role.RoleManager#ROLE_WALLET} and is category agnostic. + * * @param category The category, for example {@link #CATEGORY_PAYMENT} * @return the selection mode for the passed in category - * - * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the - * Preferred Payment service is no longer valid. All routings will be done in a AID - * category agnostic manner. */ - @Deprecated public int getSelectionModeForCategory(String category) { if (CATEGORY_PAYMENT.equals(category)) { boolean paymentRegistered = false; @@ -919,6 +921,13 @@ public final class CardEmulation { /** * Retrieves the route destination for the preferred payment service. * + * <p class="note"> + * Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the preferred payment service + * no longer exists and is replaced by {@link android.app.role.RoleManager#ROLE_WALLET}. This + * will return the route for one of the services registered by the role holder (if any). If + * there are multiple services registered, it is unspecified which of those will be used to + * determine the route. + * * @return The route destination secure element name of the preferred payment service. * HCE payment: "Host" * OffHost payment: 1. String with prefix SIM or prefix eSE string. @@ -931,15 +940,8 @@ public final class CardEmulation { * (e.g. eSE/eSE1, eSE2, etc.). * 2. "OffHost" if the payment service does not specify secure element * name. - * - * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the - * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app. - * A payment service will be selected automatically based on registered AIDs. In the case of - * multiple services that register for the same payment AID, the selection will be done on - * an alphabetical order based on the component names. */ @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) - @Deprecated @Nullable public String getRouteDestinationForPreferredPaymentService() { try { @@ -981,16 +983,16 @@ public final class CardEmulation { /** * Returns a user-visible description of the preferred payment service. * - * @return the preferred payment service description + * <p class="note"> + * Starting with {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, the preferred payment service + * no longer exists and is replaced by {@link android.app.role.RoleManager#ROLE_WALLET}. This + * will return the description for one of the services registered by the role holder (if any). + * If there are multiple services registered, it is unspecified which of those will be used + * to obtain the service description here. * - * @deprecated see {@link android.app.role.RoleManager#ROLE_WALLET}. The definition of the - * Preferred Payment service is no longer valid. All routings will go to the Wallet Holder app. - * A payment service will be selected automatically based on registered AIDs. In the case of - * multiple services that register for the same payment AID, the selection will be done on - * an alphabetical order based on the component names. + * @return the preferred payment service description */ @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) - @Deprecated @Nullable public CharSequence getDescriptionForPreferredPaymentService() { try { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt index 9111e6162cc1..68c2244f7622 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.style.Hyphens import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import com.android.compose.theme.LocalAndroidColorScheme @@ -39,7 +40,7 @@ fun HeadlineText(text: String, modifier: Modifier = Modifier) { text = text, color = LocalAndroidColorScheme.current.onSurface, textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineSmall, + style = MaterialTheme.typography.headlineSmall.copy(hyphens = Hyphens.Auto), ) } @@ -52,7 +53,7 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy(hyphens = Hyphens.Auto), ) } @@ -70,7 +71,7 @@ fun BodySmallText( modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodySmall.copy(hyphens = Hyphens.Auto), overflow = TextOverflow.Ellipsis, maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, onTextLayout = onTextLayout, @@ -86,7 +87,7 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.onSurface, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleLarge.copy(hyphens = Hyphens.Auto), ) } @@ -104,7 +105,7 @@ fun SmallTitleText( modifier = modifier.wrapContentSize(), text = text, color = LocalAndroidColorScheme.current.onSurface, - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto), overflow = TextOverflow.Ellipsis, maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, onTextLayout = onTextLayout, @@ -120,7 +121,7 @@ fun SectionHeaderText(text: String, modifier: Modifier = Modifier, color: Color) modifier = modifier.wrapContentSize(), text = text, color = color, - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto), ) } @@ -133,7 +134,7 @@ fun SnackbarContentText(text: String, modifier: Modifier = Modifier) { modifier = modifier.wrapContentSize(), text = text, color = MaterialTheme.colorScheme.inverseOnSurface, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyMedium.copy(hyphens = Hyphens.Auto), ) } @@ -146,7 +147,7 @@ fun SnackbarActionText(text: String, modifier: Modifier = Modifier) { modifier = modifier.wrapContentSize(), text = text, color = MaterialTheme.colorScheme.inversePrimary, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto), ) } @@ -160,7 +161,7 @@ fun LargeLabelTextOnSurfaceVariant(text: String, modifier: Modifier = Modifier) text = text, textAlign = TextAlign.Center, color = LocalAndroidColorScheme.current.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto), ) } @@ -173,6 +174,6 @@ fun LargeLabelText(text: String, modifier: Modifier = Modifier) { modifier = modifier.wrapContentSize(), text = text, textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto), ) }
\ No newline at end of file diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 283dc7d6fe08..0fe35e695047 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -38,7 +38,7 @@ class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() { setContent { MaterialTheme { WearApp( - viewModel = viewModel, + flowEngine = viewModel, onCloseApp = { finish() }, ) } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 9d9776301518..b7fa33e9372f 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -24,9 +24,6 @@ import com.android.credentialmanager.CredentialSelectorUiState.Get import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient import com.android.credentialmanager.model.EntryInfo -import com.android.credentialmanager.model.get.ActionEntryInfo -import com.android.credentialmanager.model.get.AuthenticationEntryInfo -import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.mappers.toGet import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult @@ -53,7 +50,7 @@ class CredentialSelectorViewModel @Inject constructor( private val shouldClose = MutableStateFlow(false) private lateinit var selectedEntry: EntryInfo private var isAutoSelected: Boolean = false - val uiState: StateFlow<CredentialSelectorUiState> = + override val uiState: StateFlow<CredentialSelectorUiState> = combine( credentialManagerClient.requests, isPrimaryScreen, @@ -137,29 +134,3 @@ class CredentialSelectorViewModel @Inject constructor( } } -sealed class CredentialSelectorUiState { - data object Idle : CredentialSelectorUiState() - sealed class Get : CredentialSelectorUiState() { - data class SingleEntry(val entry: CredentialEntryInfo) : Get() - data class SingleEntryPerAccount( - val sortedEntries: List<CredentialEntryInfo>, - val authenticationEntryList: List<AuthenticationEntryInfo>, - ) : Get() - data class MultipleEntry( - val accounts: List<PerUserNameEntries>, - val actionEntryList: List<ActionEntryInfo>, - val authenticationEntryList: List<AuthenticationEntryInfo>, - ) : Get() { - data class PerUserNameEntries( - val userName: String, - val sortedCredentialEntryList: List<CredentialEntryInfo>, - ) - } - - // TODO: b/301206470 add the remaining states - } - - data object Create : CredentialSelectorUiState() - data class Cancel(val appName: String) : CredentialSelectorUiState() - data object Close : CredentialSelectorUiState() -} diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt index 2e80a7c672f4..c05fc93b8223 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt @@ -20,9 +20,15 @@ import android.content.Intent import androidx.activity.result.IntentSenderRequest import androidx.compose.runtime.Composable import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.model.get.ActionEntryInfo +import com.android.credentialmanager.model.get.AuthenticationEntryInfo +import com.android.credentialmanager.model.get.CredentialEntryInfo +import kotlinx.coroutines.flow.StateFlow /** Engine of the credential selecting flow. */ interface FlowEngine { + /** UI state of the selector app */ + val uiState: StateFlow<CredentialSelectorUiState> /** Back from previous stage. */ fun back() /** Cancels the selection flow. */ @@ -54,4 +60,40 @@ interface FlowEngine { */ @Composable fun getEntrySelector(): (entry: EntryInfo, isAutoSelected: Boolean) -> Unit +} + +/** UI state of the selector app */ +sealed class CredentialSelectorUiState { + /** Idle UI state, no request is going on. */ + data object Idle : CredentialSelectorUiState() + /** Getting credential UI state. */ + sealed class Get : CredentialSelectorUiState() { + /** Getting credential UI state when there is only one credential available. */ + data class SingleEntry(val entry: CredentialEntryInfo) : Get() + /** + * Getting credential UI state when there is only one account while with multiple + * credentials, with different types(eg, passkey vs password) or providers. + */ + data class SingleEntryPerAccount( + val sortedEntries: List<CredentialEntryInfo>, + val authenticationEntryList: List<AuthenticationEntryInfo>, + ) : Get() + /** Getting credential UI state when there are multiple accounts available. */ + data class MultipleEntry( + val accounts: List<PerUserNameEntries>, + val actionEntryList: List<ActionEntryInfo>, + val authenticationEntryList: List<AuthenticationEntryInfo>, + ) : Get() { + data class PerUserNameEntries( + val userName: String, + val sortedCredentialEntryList: List<CredentialEntryInfo>, + ) + } + } + /** Creating credential UI state. */ + data object Create : CredentialSelectorUiState() + /** Request is cancelling by [appName]. */ + data class Cancel(val appName: String) : CredentialSelectorUiState() + /** Request is closed peacefully. */ + data object Close : CredentialSelectorUiState() }
\ No newline at end of file diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt index bf4c988679b9..018db6899f6e 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -32,7 +32,6 @@ import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntryPerAccount import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry -import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.FlowEngine import com.android.credentialmanager.TAG import com.android.credentialmanager.ui.screens.LoadingScreen @@ -52,8 +51,7 @@ import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFlatten @OptIn(ExperimentalHorologistApi::class) @Composable fun WearApp( - viewModel: CredentialSelectorViewModel, - flowEngine: FlowEngine = viewModel, + flowEngine: FlowEngine, onCloseApp: () -> Unit, ) { val navController = rememberSwipeDismissableNavController() @@ -62,7 +60,7 @@ fun WearApp( rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState) val selectEntry = flowEngine.getEntrySelector() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState by flowEngine.uiState.collectAsStateWithLifecycle() WearNavScaffold( startDestination = Screen.Loading.route, navController = navController, @@ -112,7 +110,7 @@ fun WearApp( } } BackHandler(true) { - viewModel.back() + flowEngine.back() } Log.d(TAG, "uiState change, state: $uiState") when (val state = uiState) { diff --git a/packages/SettingsLib/Spa/build.gradle.kts b/packages/SettingsLib/Spa/build.gradle.kts index c755623c6f08..4147813bd059 100644 --- a/packages/SettingsLib/Spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/build.gradle.kts @@ -29,7 +29,7 @@ val androidTop: String = File(rootDir, "../../../../..").canonicalPath allprojects { extra["androidTop"] = androidTop - extra["jetpackComposeVersion"] = "1.7.0-alpha04" + extra["jetpackComposeVersion"] = "1.7.0-alpha05" } subprojects { diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt index 247990c69cd8..f1cbc3729a78 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt @@ -17,6 +17,8 @@ package com.android.settingslib.spa.gallery.page import android.os.Bundle +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text @@ -32,6 +34,7 @@ import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel @@ -72,10 +75,11 @@ object LoadingBarPageProvider : SettingsPageProvider { Text(text = "Resume") } } + Spacer(modifier = Modifier.height(SettingsDimension.itemPaddingVertical)) + LinearLoadingBar(isLoading = loading) + Spacer(modifier = Modifier.height(SettingsDimension.itemPaddingVertical)) + CircularLoadingBar(isLoading = loading) } - - LinearLoadingBar(isLoading = loading, yOffset = 104.dp) - CircularLoadingBar(isLoading = loading) } } diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index ff2a1e8d8184..0ee9d595d875 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,11 +15,11 @@ # [versions] -agp = "8.3.0" -compose-compiler = "1.5.10" +agp = "8.3.1" +compose-compiler = "1.5.11" dexmaker-mockito = "2.28.3" jvm = "17" -kotlin = "1.9.22" +kotlin = "1.9.23" truth = "1.1.5" [libraries] diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index f2b9235e92b4..2f2ac2467a6c 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -57,13 +57,13 @@ dependencies { api("androidx.slice:slice-builders:1.1.0-alpha02") api("androidx.slice:slice-core:1.1.0-alpha02") api("androidx.slice:slice-view:1.1.0-alpha02") - api("androidx.compose.material3:material3:1.3.0-alpha02") + api("androidx.compose.material3:material3:1.3.0-alpha03") api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion") api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion") api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion") api("androidx.lifecycle:lifecycle-livedata-ktx") api("androidx.lifecycle:lifecycle-runtime-compose") - api("androidx.navigation:navigation-compose:2.8.0-alpha03") + api("androidx.navigation:navigation-compose:2.8.0-alpha05") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") api("com.google.android.material:material:1.7.0-alpha03") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt index 0e7e49960be1..2de73c0ec289 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt @@ -19,14 +19,19 @@ package com.android.settingslib.spa.widget.editor import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled @@ -110,22 +115,31 @@ private fun CheckboxItem( option: SettingsDropdownCheckOption, onClick: (SettingsDropdownCheckOption) -> Unit, ) { - TextButton( - onClick = { onClick(option) }, - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = option.selected.value, - onCheckedChange = null, + Row( + modifier = Modifier + .fillMaxWidth() + .minimumInteractiveComponentSize() + .toggleable( + value = option.selected.value, enabled = option.changeable, + role = Role.Checkbox, + onValueChange = { onClick(option) }, ) - Text(text = option.text, modifier = Modifier.alphaForEnabled(option.changeable)) - } + .padding(ButtonDefaults.TextButtonContentPadding), + horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = option.selected.value, + onCheckedChange = null, + enabled = option.changeable, + ) + Text( + text = option.text, + modifier = Modifier.alphaForEnabled(option.changeable), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + ) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt index 354b95ddcbfe..f372a45f9e59 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SuwScaffold.kt @@ -40,7 +40,8 @@ import com.android.settingslib.spa.framework.theme.toMediumWeight data class BottomAppBarButton( val text: String, - val onClick: () -> Unit, + val enabled: Boolean = true, + val onClick: () -> Unit ) @Composable @@ -122,13 +123,13 @@ private fun BottomBar( ) { Row(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) { dismissButton?.apply { - TextButton(onClick) { + TextButton(onClick = onClick, enabled = enabled) { ActionText(text) } } Spacer(modifier = Modifier.weight(1f)) actionButton?.apply { - Button(onClick) { + Button(onClick = onClick, enabled = enabled) { ActionText(text) } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt index 1741f134f3d1..be178ff26f1f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/LoadingBar.kt @@ -17,7 +17,6 @@ package com.android.settingslib.spa.widget.ui import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.CircularProgressIndicator @@ -25,23 +24,15 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp /** * Indeterminate linear progress bar. Expresses an unspecified wait time. */ @Composable -fun LinearLoadingBar( - isLoading: Boolean, - xOffset: Dp = 0.dp, - yOffset: Dp = 0.dp -) { +fun LinearLoadingBar(isLoading: Boolean) { if (isLoading) { LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .absoluteOffset(xOffset, yOffset) + modifier = Modifier.fillMaxWidth() ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt index a12f0990b581..acd9e3dc83cb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.composable.blueprint +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneKey @@ -39,7 +40,7 @@ object ClockTransition { transitioningToSmallClock() } from(ClockScenes.splitShadeLargeClockScene, to = ClockScenes.largeClockScene) { - spec = tween(1000) + spec = tween(1000, easing = LinearEasing) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 2781f39fc479..1c938a6c19a5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.composable.section +import android.content.res.Resources import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -23,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -36,6 +38,8 @@ import com.android.systemui.customization.R as customizationR import com.android.systemui.customization.R import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey +import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene +import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene import com.android.systemui.keyguard.ui.composable.modifier.burnInAware import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel @@ -95,6 +99,36 @@ constructor( if (currentClock?.largeClock?.view == null) { return } + + // Centering animation for clocks that have custom position animations. + LaunchedEffect(layoutState.currentTransition?.progress) { + val transition = layoutState.currentTransition ?: return@LaunchedEffect + if (currentClock?.largeClock?.config?.hasCustomPositionUpdatedAnimation != true) { + return@LaunchedEffect + } + + // If we are not doing the centering animation, do not animate. + val progress = + if (transition.isTransitioningBetween(largeClockScene, splitShadeLargeClockScene)) { + transition.progress + } else { + 1f + } + + val distance = + if (transition.toScene == splitShadeLargeClockScene) { + -getClockCenteringDistance() + } else { + getClockCenteringDistance() + } + .toFloat() + val largeClock = checkNotNull(currentClock).largeClock + largeClock.animations.onPositionUpdated( + distance = distance, + fraction = progress, + ) + } + MovableElement(key = largeClockElementKey, modifier = modifier) { content { AndroidView( @@ -120,4 +154,8 @@ constructor( (clockView.parent as? ViewGroup)?.removeView(clockView) addView(clockView) } + + fun getClockCenteringDistance(): Float { + return Resources.getSystem().displayMetrics.widthPixels / 4f + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index d72d5cad31b4..b4472fc15ac4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -16,12 +16,16 @@ package com.android.systemui.keyguard.ui.composable.section +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -31,11 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.modifiers.thenIf import com.android.systemui.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene @@ -63,6 +68,9 @@ constructor( ) { val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsState() val currentClockLayout by clockViewModel.currentClockLayout.collectAsState() + val hasCustomPositionUpdatedAnimation by + clockViewModel.hasCustomPositionUpdatedAnimation.collectAsState() + val currentScene = when (currentClockLayout) { KeyguardClockViewModel.ClockLayout.SPLIT_SHADE_LARGE_CLOCK -> @@ -94,12 +102,10 @@ constructor( transitions = ClockTransition.defaultClockTransitions, enableInterruptions = false, ) { - scene(ClockScenes.splitShadeLargeClockScene) { - Row( - modifier = Modifier.fillMaxSize(), - ) { + scene(splitShadeLargeClockScene) { + Box(modifier = Modifier.fillMaxSize()) { Column( - modifier = Modifier.fillMaxHeight().weight(weight = 1f), + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { with(smartSpaceSection) { @@ -108,8 +114,34 @@ constructor( onTopChanged = burnIn.onSmartspaceTopChanged, ) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + + with(clockSection) { + LargeClock( + modifier = + Modifier.fillMaxSize().thenIf( + !hasCustomPositionUpdatedAnimation + ) { + // If we do not have a custom position animation, we want + // the clock to be on one half of the screen. + Modifier.offset { + IntOffset( + x = + -clockSection + .getClockCenteringDistance() + .toInt(), + y = 0, + ) + } + } + ) + } } + } + + Row( + modifier = Modifier.fillMaxSize(), + ) { + Spacer(modifier = Modifier.weight(weight = 1f)) with(notificationSection) { Notifications( modifier = @@ -121,7 +153,7 @@ constructor( } } - scene(ClockScenes.splitShadeSmallClockScene) { + scene(splitShadeSmallClockScene) { Row( modifier = Modifier.fillMaxSize(), ) { @@ -133,7 +165,7 @@ constructor( SmallClock( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.wrapContentSize() ) } with(smartSpaceSection) { @@ -155,13 +187,13 @@ constructor( } } - scene(ClockScenes.smallClockScene) { + scene(smallClockScene) { Column { with(clockSection) { SmallClock( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmallClockTopChanged, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.wrapContentSize() ) } with(smartSpaceSection) { @@ -172,15 +204,12 @@ constructor( } with(mediaCarouselSection) { MediaCarousel() } with(notificationSection) { - Notifications( - modifier = - androidx.compose.ui.Modifier.fillMaxWidth().weight(weight = 1f) - ) + Notifications(modifier = Modifier.fillMaxWidth().weight(weight = 1f)) } } } - scene(ClockScenes.largeClockScene) { + scene(largeClockScene) { Column { with(smartSpaceSection) { SmartSpace( @@ -188,7 +217,7 @@ constructor( onTopChanged = burnIn.onSmartspaceTopChanged, ) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + with(clockSection) { LargeClock(modifier = Modifier.fillMaxSize()) } } } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index cea49e1b535e..11c946261816 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -517,24 +517,12 @@ class AnimatableClockView @JvmOverloads constructor( val currentMoveAmount = left - clockStartLeft val digitOffsetDirection = if (isLayoutRtl) -1 else 1 for (i in 0 until NUM_DIGITS) { - // The delay for the digit, in terms of fraction (i.e. the digit should not move - // during 0.0 - 0.1). - val digitInitialDelay = - if (isMovingToCenter) { - moveToCenterDelays[i] * MOVE_DIGIT_STEP - } else { - moveToSideDelays[i] * MOVE_DIGIT_STEP - } val digitFraction = - MOVE_INTERPOLATOR.getInterpolation( - constrainedMap( - 0.0f, - 1.0f, - digitInitialDelay, - digitInitialDelay + AVAILABLE_ANIMATION_TIME, - moveFraction - ) - ) + getDigitFraction( + digit = i, + isMovingToCenter = isMovingToCenter, + fraction = moveFraction, + ) val moveAmountForDigit = currentMoveAmount * digitFraction val moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount glyphOffsets[i] = digitOffsetDirection * moveAmountDeltaForDigit @@ -542,6 +530,57 @@ class AnimatableClockView @JvmOverloads constructor( invalidate() } + /** + * Offsets the glyphs of the clock for the step clock animation. + * + * The animation makes the glyphs of the clock move at different speeds, when the clock is + * moving horizontally. This method uses direction, distance, and fraction to determine offset. + * + * @param distance is the total distance in pixels to offset the glyphs when animation + * completes. Negative distance means we are animating the position towards the center. + * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 + * means it finished moving. + */ + fun offsetGlyphsForStepClockAnimation( + distance: Float, + fraction: Float, + ) { + for (i in 0 until NUM_DIGITS) { + val dir = if (isLayoutRtl) -1 else 1 + val digitFraction = + getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction) + val moveAmountForDigit = dir * distance * digitFraction + glyphOffsets[i] = moveAmountForDigit + + if (distance > 0) { + // If distance > 0 then we are moving from the left towards the center. + // We need ensure that the glyphs are offset to the initial position. + glyphOffsets -= dir * distance + } + } + invalidate() + } + + private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float { + // The delay for the digit, in terms of fraction (i.e. the digit should not move + // during 0.0 - 0.1). + val digitInitialDelay = + if (isMovingToCenter) { + moveToCenterDelays[digit] * MOVE_DIGIT_STEP + } else { + moveToSideDelays[digit] * MOVE_DIGIT_STEP + } + return MOVE_INTERPOLATOR.getInterpolation( + constrainedMap( + 0.0f, + 1.0f, + digitInitialDelay, + digitInitialDelay + AVAILABLE_ANIMATION_TIME, + fraction, + ) + ) + } + // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. // This is an optimization to ensure we only recompute the patterns when the inputs change. private object Patterns { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index 54c7a0823963..b39201427b46 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -232,6 +232,10 @@ class DefaultClockController( fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) { view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) } + + fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) { + view.offsetGlyphsForStepClockAnimation(distance, fraction) + } } inner class DefaultClockEvents : ClockEvents { @@ -316,6 +320,8 @@ class DefaultClockController( } override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {} + + override fun onPositionUpdated(distance: Float, fraction: Float) {} } inner class LargeClockAnimations( @@ -326,6 +332,10 @@ class DefaultClockController( override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) { largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction) } + + override fun onPositionUpdated(distance: Float, fraction: Float) { + largeClock.offsetGlyphsForStepClockAnimation(distance, fraction) + } } class AnimationState( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index f8321b7e7eb3..07e9815fb5a5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -60,9 +60,12 @@ import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR @@ -158,6 +161,9 @@ class SideFpsControllerTest : SysuiTestCase() { FakeBiometricSettingsRepository(), FakeSystemClock(), mock(KeyguardUpdateMonitor::class.java), + { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + { mock(KeyguardInteractor::class.java) }, + { mock(KeyguardTransitionInteractor::class.java) }, testScope.backgroundScope, ) displayStateInteractor = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt index b253309104d6..d88260f0760a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt @@ -24,14 +24,18 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.SystemClock +import dagger.Lazy import kotlinx.coroutines.test.TestScope import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -39,6 +43,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations @SmallTest @@ -81,10 +86,19 @@ class AlternateBouncerInteractorTest : SysuiTestCase() { biometricSettingsRepository, systemClock, keyguardUpdateMonitor, + Lazy { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + Lazy { mock(KeyguardInteractor::class.java) }, + Lazy { mock(KeyguardTransitionInteractor::class.java) }, TestScope().backgroundScope, ) } + @Test(expected = IllegalStateException::class) + fun enableUdfpsRefactor_deprecatedShowMethod_throwsIllegalStateException() { + mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) + underTest.show() + } + @Test fun canShowAlternateBouncerForFingerprint_givenCanShow() { givenCanShowAlternateBouncer() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index c308a987455b..2a2b2f1892f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -85,7 +85,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testIsImportantForAccessibility_falseWhenNoNotifs() = + fun isImportantForAccessibility_falseWhenNoNotifs() = testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) @@ -100,7 +100,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testIsImportantForAccessibility_trueWhenNotifs() = + fun isImportantForAccessibility_trueWhenNotifs() = testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) @@ -115,7 +115,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testIsImportantForAccessibility_trueWhenNotKeyguard() = + fun isImportantForAccessibility_trueWhenNotKeyguard() = testScope.runTest { val important by collectLastValue(underTest.isImportantForAccessibility) @@ -130,7 +130,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() = + fun shouldIncludeEmptyShadeView_trueWhenNoNotifs() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -143,7 +143,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_falseWhenNotifs() = + fun shouldIncludeEmptyShadeView_falseWhenNotifs() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -156,7 +156,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() = + fun shouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -171,7 +171,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() = + fun shouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -189,7 +189,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() = + fun shouldIncludeEmptyShadeView_trueWhenLockedShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -204,7 +204,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() = + fun shouldIncludeEmptyShadeView_falseWhenKeyguard() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -219,7 +219,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() = + fun shouldIncludeEmptyShadeView_falseWhenStartingToSleep() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) @@ -236,7 +236,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testAreNotificationsHiddenInShade_true() = + fun areNotificationsHiddenInShade_true() = testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) @@ -248,7 +248,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testAreNotificationsHiddenInShade_false() = + fun areNotificationsHiddenInShade_false() = testScope.runTest { val hidden by collectLastValue(underTest.areNotificationsHiddenInShade) @@ -260,7 +260,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testHasFilteredOutSeenNotifications_true() = + fun hasFilteredOutSeenNotifications_true() = testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) @@ -271,7 +271,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testHasFilteredOutSeenNotifications_false() = + fun hasFilteredOutSeenNotifications_false() = testScope.runTest { val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications) @@ -282,7 +282,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_trueWhenShade() = + fun shouldIncludeFooterView_trueWhenShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -298,7 +298,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_trueWhenLockedShade() = + fun shouldIncludeFooterView_trueWhenLockedShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -314,7 +314,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_falseWhenKeyguard() = + fun shouldIncludeFooterView_falseWhenKeyguard() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -329,7 +329,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_falseWhenUserNotSetUp() = + fun shouldIncludeFooterView_falseWhenUserNotSetUp() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -347,7 +347,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_falseWhenStartingToSleep() = + fun shouldIncludeFooterView_falseWhenStartingToSleep() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -365,7 +365,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() = + fun shouldIncludeFooterView_falseWhenQsExpandedDefault() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -384,7 +384,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() = + fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -405,7 +405,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_falseWhenRemoteInputActive() = + fun shouldIncludeFooterView_falseWhenRemoteInputActive() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -423,7 +423,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_animatesWhenShade() = + fun shouldIncludeFooterView_animatesWhenShade() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -439,7 +439,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldIncludeFooterView_notAnimatingOnKeyguard() = + fun shouldIncludeFooterView_notAnimatingOnKeyguard() = testScope.runTest { val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) @@ -455,7 +455,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldHideFooterView_trueWhenShadeIsClosed() = + fun shouldHideFooterView_trueWhenShadeIsClosed() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -469,7 +469,7 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldHideFooterView_falseWhenShadeIsOpen() = + fun shouldHideFooterView_falseWhenShadeIsOpen() = testScope.runTest { val shouldHide by collectLastValue(underTest.shouldHideFooterView) @@ -484,7 +484,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testPinnedHeadsUpRows_filtersForPinnedItems() = + fun pinnedHeadsUpRows_filtersForPinnedItems() = testScope.runTest { val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) @@ -527,7 +527,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testHasPinnedHeadsUpRows_true() = + fun hasPinnedHeadsUpRows_true() = testScope.runTest { val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) @@ -542,7 +542,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testHasPinnedHeadsUpRows_false() = + fun hasPinnedHeadsUpRows_false() = testScope.runTest { val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) @@ -557,7 +557,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testTopHeadsUpRow_emptyList_null() = + fun topHeadsUpRow_emptyList_null() = testScope.runTest { val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow) @@ -569,7 +569,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testHeadsUpAnimationsEnabled_true() = + fun headsUpAnimationsEnabled_true() = testScope.runTest { val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) @@ -582,7 +582,7 @@ class NotificationListViewModelTest : SysuiTestCase() { @Test @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) - fun testHeadsUpAnimationsEnabled_keyguardShowing_false() = + fun headsUpAnimationsEnabled_keyguardShowing_false() = testScope.runTest { val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) 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 5256bb956bc4..f969ee677763 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 @@ -290,10 +290,15 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { testScope.runTest { val alpha by collectLastValue(underTest.glanceableHubAlpha) - // Start on dream - showDream() + // Start on lockscreen, notifications should be unhidden. + showLockscreen() assertThat(alpha).isEqualTo(1f) + // Transition to dream, notifications should be hidden so that transition + // from dream->hub doesn't cause notification flicker. + showDream() + assertThat(alpha).isEqualTo(0f) + // Start transitioning to glanceable hub val progress = 0.6f keyguardTransitionRepository.sendTransitionStep( diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index fd7a7f34d258..8e2bd9b2562b 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -188,10 +188,21 @@ interface ClockAnimations { * negative means left. * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means * it finished moving. + * @deprecated use {@link #onPositionUpdated(float, float)} instead. */ fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) /** + * Runs when the clock's position changed during the move animation. + * + * @param distance is the total distance in pixels to offset the glyphs when animation + * completes. Negative distance means we are animating the position towards the center. + * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means + * it finished moving. + */ + fun onPositionUpdated(distance: Float, fraction: Float) + + /** * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview, * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt index 188e7ee439fd..7525ce0f98ac 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt @@ -21,16 +21,27 @@ import com.android.systemui.biometrics.data.repository.FingerprintPropertyReposi import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.time.SystemClock +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -46,6 +57,9 @@ constructor( private val biometricSettingsRepository: BiometricSettingsRepository, private val systemClock: SystemClock, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val deviceEntryFingerprintAuthInteractor: Lazy<DeviceEntryFingerprintAuthInteractor>, + private val keyguardInteractor: Lazy<KeyguardInteractor>, + keyguardTransitionInteractor: Lazy<KeyguardTransitionInteractor>, @Application scope: CoroutineScope, ) { var receivedDownTouch = false @@ -63,13 +77,80 @@ constructor( } else { bouncerRepository.alternateBouncerUIAvailable } + private val isDozingOrAod: Flow<Boolean> = + keyguardTransitionInteractor + .get() + .transitions + .map { + it.to == KeyguardState.DOZING || + it.to == KeyguardState.AOD || + ((it.from == KeyguardState.DOZING || it.from == KeyguardState.AOD) && + it.transitionState != TransitionState.FINISHED) + } + .distinctUntilChanged() + + /** + * Whether the current biometric, bouncer, and keyguard states allow the alternate bouncer to + * show. + */ + val canShowAlternateBouncer: StateFlow<Boolean> = + alternateBouncerSupported + .flatMapLatest { alternateBouncerSupported -> + if (alternateBouncerSupported) { + keyguardTransitionInteractor.get().currentKeyguardState.flatMapLatest { + currentKeyguardState -> + if (currentKeyguardState == KeyguardState.GONE) { + flowOf(false) + } else { + combine( + deviceEntryFingerprintAuthInteractor + .get() + .isFingerprintAuthCurrentlyAllowed, + keyguardInteractor.get().isKeyguardDismissible, + bouncerRepository.primaryBouncerShow, + isDozingOrAod + ) { + fingerprintAllowed, + keyguardDismissible, + primaryBouncerShowing, + dozing -> + fingerprintAllowed && + !keyguardDismissible && + !primaryBouncerShowing && + !dozing + } + } + } + } else { + flowOf(false) + } + } + .stateIn( + scope = scope, + started = WhileSubscribed(), + initialValue = false, + ) + + /** + * Always shows the alternate bouncer. Requesters must check [canShowAlternateBouncer]` before + * calling this. + */ + fun forceShow() { + if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) { + show() + return + } + bouncerRepository.setAlternateVisible(true) + } /** * Sets the correct bouncer states to show the alternate bouncer if it can show. * * @return whether alternateBouncer is visible + * @deprecated use [forceShow] and manually check [canShowAlternateBouncer] beforehand */ fun show(): Boolean { + DeviceEntryUdfpsRefactor.assertInLegacyMode() bouncerRepository.setAlternateVisible(canShowAlternateBouncerForFingerprint()) return isVisibleState() } @@ -105,6 +186,9 @@ constructor( } fun canShowAlternateBouncerForFingerprint(): Boolean { + if (DeviceEntryUdfpsRefactor.isEnabled) { + return canShowAlternateBouncer.value + } return alternateBouncerSupported.value && biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed.value && !keyguardUpdateMonitor.isFingerprintLockedOut && diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index a293afcc28dc..182798e097d3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -139,6 +139,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dreams.ui.viewmodel.DreamViewModel; import com.android.systemui.dump.DumpManager; @@ -3404,7 +3405,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Ensure that keyguard becomes visible if the going away animation is canceled if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() - && MigrateClocksToBlueprint.isEnabled()) { + && (MigrateClocksToBlueprint.isEnabled() + || DeviceEntryUdfpsRefactor.isEnabled())) { mKeyguardInteractor.showKeyguard(); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 1c1c33ab7e7e..3d649512f342 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -130,6 +130,17 @@ constructor( initialValue = ClockLayout.SMALL_CLOCK ) + val hasCustomPositionUpdatedAnimation: StateFlow<Boolean> = + combine(currentClock, isLargeClockVisible) { currentClock, isLargeClockVisible -> + isLargeClockVisible && + currentClock?.largeClock?.config?.hasCustomPositionUpdatedAnimation == true + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + /** Calculates the top margin for the small clock. */ fun getSmallClockTopMargin(context: Context): Int { var topMargin: Int diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 18bb51197555..5544f9389787 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -173,7 +173,6 @@ constructor( footerView, footerViewModel, clearAllNotifications = { - metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES) clearAllNotifications( parentView, // Hide the silent section header (if present) if there will be 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 ecf737a8650f..4a096a89848a 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 @@ -20,6 +20,7 @@ import android.view.View import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags.communalHub import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -150,9 +151,11 @@ constructor( } } - launch { - viewModel.glanceableHubAlpha.collect { - controller.setMaxAlphaForGlanceableHub(it) + if (communalHub()) { + launch { + viewModel.glanceableHubAlpha.collect { + controller.setMaxAlphaForGlanceableHub(it) + } } } } 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 d112edb9772c..f767b9997864 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 @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN @@ -241,7 +242,22 @@ constructor( started = SharingStarted.Eagerly, initialValue = false, ) - .dumpWhileCollecting("isOnGlanceableHubWithoutShade") + .dumpValue("isOnGlanceableHubWithoutShade") + + /** Are we on the dream without the shade/qs? */ + private val isDreamingWithoutShade: Flow<Boolean> = + combine( + keyguardTransitionInteractor.isFinishedInState(DREAMING), + isAnyExpanded, + ) { isDreaming, isAnyExpanded -> + isDreaming && !isAnyExpanded + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + .dumpValue("isDreamingWithoutShade") /** * Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is @@ -460,6 +476,7 @@ constructor( combineTransform( isOnGlanceableHubWithoutShade, isOnLockscreen, + isDreamingWithoutShade, merge( lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, glanceableHubToLockscreenTransitionViewModel.notificationAlpha, @@ -467,9 +484,9 @@ constructor( // Manually emit on start because [notificationAlpha] only starts emitting // when transitions start. .onStart { emit(1f) } - ) { isOnGlanceableHubWithoutShade, isOnLockscreen, alpha, + ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha, -> - if (isOnGlanceableHubWithoutShade && !isOnLockscreen) { + if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) { // Notifications should not be visible on the glanceable hub. // TODO(b/321075734): implement a way to actually set the notifications to // gone while on the hub instead of just adjusting alpha @@ -484,6 +501,7 @@ constructor( emit(1f) } } + .distinctUntilChanged() .dumpWhileCollecting("glanceableHubAlpha") /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 97fc35a062f4..8b7b348ede76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -51,6 +51,9 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; @@ -59,6 +62,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.SystemClock; import dagger.Lazy; @@ -286,7 +290,9 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp VibratorHelper vibrator, SystemClock systemClock, Lazy<SelectedUserInteractor> selectedUserInteractor, - BiometricUnlockInteractor biometricUnlockInteractor + BiometricUnlockInteractor biometricUnlockInteractor, + JavaAdapter javaAdapter, + KeyguardTransitionInteractor keyguardTransitionInteractor ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; @@ -317,10 +323,19 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mOrderUnlockAndWake = resources.getBoolean( com.android.internal.R.bool.config_orderUnlockAndWake); mSelectedUserInteractor = selectedUserInteractor; - + javaAdapter.alwaysCollectFlow( + keyguardTransitionInteractor.getStartedKeyguardTransitionStep(), + this::consumeTransitionStepOnStartedKeyguardState); dumpManager.registerDumpable(this); } + @VisibleForTesting + protected void consumeTransitionStepOnStartedKeyguardState(TransitionStep transitionStep) { + if (transitionStep.getFrom() == KeyguardState.GONE) { + mBiometricUnlockInteractor.setBiometricUnlockState(MODE_NONE); + } + } + public void setKeyguardViewController(KeyguardViewController keyguardViewController) { mKeyguardViewController = keyguardViewController; } @@ -773,7 +788,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp for (BiometricUnlockEventsListener listener : mBiometricUnlockEventsListeners) { listener.onResetMode(); } - mBiometricUnlockInteractor.setBiometricUnlockState(MODE_NONE); mNumConsecutiveFpFailures = 0; mLastFpFailureUptimeMillis = 0; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index a99834ad3456..febe5a25d3aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -169,6 +169,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private Job mListenForAlternateBouncerTransitionSteps = null; private Job mListenForKeyguardAuthenticatedBiometricsHandled = null; + private Job mListenForCanShowAlternateBouncer = null; // Local cache of expansion events, to avoid duplicates private float mFraction = -1f; @@ -506,6 +507,10 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mListenForKeyguardAuthenticatedBiometricsHandled.cancel(null); } mListenForKeyguardAuthenticatedBiometricsHandled = null; + if (mListenForCanShowAlternateBouncer != null) { + mListenForCanShowAlternateBouncer.cancel(null); + } + mListenForCanShowAlternateBouncer = null; if (!DeviceEntryUdfpsRefactor.isEnabled()) { mListenForAlternateBouncerTransitionSteps = mJavaAdapter.alwaysCollectFlow( mKeyguardTransitionInteractor.transitionStepsFromState( @@ -517,6 +522,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mPrimaryBouncerInteractor.getKeyguardAuthenticatedBiometricsHandled(), this::consumeKeyguardAuthenticatedBiometricsHandled ); + } else { + mListenForCanShowAlternateBouncer = mJavaAdapter.alwaysCollectFlow( + mAlternateBouncerInteractor.getCanShowAlternateBouncer(), + this::consumeCanShowAlternateBouncer + ); } if (KeyguardWmStateRefactor.isEnabled()) { @@ -558,6 +568,11 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb } } + private void consumeCanShowAlternateBouncer(boolean canShow) { + // do nothing, we only are registering for the flow to ensure that there's at least + // one subscriber that will update AlternateBouncerInteractor.canShowAlternateBouncer.value + } + /** Register a callback, to be invoked by the Predictive Back system. */ private void registerBackCallback() { if (!mIsBackCallbackRegistered) { @@ -723,6 +738,16 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb * {@see KeyguardBouncer#show(boolean, boolean)} */ public void showBouncer(boolean scrimmed) { + if (DeviceEntryUdfpsRefactor.isEnabled()) { + if (mAlternateBouncerInteractor.canShowAlternateBouncerForFingerprint()) { + mAlternateBouncerInteractor.forceShow(); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState()); + } else { + showPrimaryBouncer(scrimmed); + } + return; + } + if (!mAlternateBouncerInteractor.show()) { showPrimaryBouncer(scrimmed); } else { @@ -834,7 +859,12 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardGoneCancelAction = null; } - updateAlternateBouncerShowing(mAlternateBouncerInteractor.show()); + if (DeviceEntryUdfpsRefactor.isEnabled()) { + mAlternateBouncerInteractor.forceShow(); + updateAlternateBouncerShowing(mAlternateBouncerInteractor.isVisibleState()); + } else { + updateAlternateBouncerShowing(mAlternateBouncerInteractor.show()); + } setKeyguardMessage(message, null, null); return; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 5509c048b0da..d4a0c8f74b14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -58,6 +58,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry @@ -66,6 +67,8 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintA import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel @@ -193,6 +196,9 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { biometricSettingsRepository, FakeSystemClock(), keyguardUpdateMonitor, + { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + { mock(KeyguardInteractor::class.java) }, + { mock(KeyguardTransitionInteractor::class.java) }, testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 2014755bd964..ae20c703b93d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -55,6 +55,7 @@ import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry @@ -63,6 +64,8 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintA import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel @@ -191,6 +194,9 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { biometricSettingsRepository, FakeSystemClock(), keyguardUpdateMonitor, + { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + { mock(KeyguardInteractor::class.java) }, + { mock(KeyguardTransitionInteractor::class.java) }, testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt index cb8c40c333b3..3b4f683124e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository @@ -108,6 +109,9 @@ class DeviceEntrySideFpsOverlayInteractorTest : SysuiTestCase() { biometricSettingsRepository, FakeSystemClock(), keyguardUpdateMonitor, + { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + { mock(KeyguardInteractor::class.java) }, + { mock(KeyguardTransitionInteractor::class.java) }, testScope.backgroundScope, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt index e53cd11ebe48..d12980a74a18 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt @@ -20,17 +20,23 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockFaceConfig +import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock @SmallTest @RunWith(JUnit4::class) @@ -98,4 +104,49 @@ class KeyguardClockViewModelWithKosmosTest : SysuiTestCase() { val currentClockLayout by collectLastValue(underTest.currentClockLayout) assertThat(currentClockLayout).isEqualTo(KeyguardClockViewModel.ClockLayout.LARGE_CLOCK) } + + @Test + fun hasCustomPositionUpdatedAnimation_withConfigTrue_isTrue() = + testScope.runTest { + with(kosmos) { + keyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE) + fakeKeyguardClockRepository.setCurrentClock( + buildClockController(hasCustomPositionUpdatedAnimation = true) + ) + } + + val hasCustomPositionUpdatedAnimation by + collectLastValue(underTest.hasCustomPositionUpdatedAnimation) + assertThat(hasCustomPositionUpdatedAnimation).isEqualTo(true) + } + + @Test + fun hasCustomPositionUpdatedAnimation_withConfigFalse_isFalse() = + testScope.runTest { + with(kosmos) { + keyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE) + fakeKeyguardClockRepository.setCurrentClock( + buildClockController(hasCustomPositionUpdatedAnimation = false) + ) + } + + val hasCustomPositionUpdatedAnimation by + collectLastValue(underTest.hasCustomPositionUpdatedAnimation) + assertThat(hasCustomPositionUpdatedAnimation).isEqualTo(false) + } + + private fun buildClockController( + hasCustomPositionUpdatedAnimation: Boolean = false + ): ClockController { + val clockController = mock(ClockController::class.java) + val largeClock = mock(ClockFaceController::class.java) + val config = mock(ClockFaceConfig::class.java) + + whenever(clockController.largeClock).thenReturn(largeClock) + whenever(largeClock.config).thenReturn(config) + whenever(config.hasCustomPositionUpdatedAnimation) + .thenReturn(hasCustomPositionUpdatedAnimation) + + return clockController + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 6f65eb426e64..50f81ff13825 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; import static com.google.common.truth.Truth.assertThat; @@ -26,6 +27,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @@ -51,6 +53,10 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; +import com.android.systemui.keyguard.shared.model.TransitionState; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.NotificationMediaManager; @@ -58,6 +64,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; @@ -158,7 +165,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, mSystemClock, () -> mSelectedUserInteractor, - mBiometricUnlockInteractor + mBiometricUnlockInteractor, + mock(JavaAdapter.class), + mock(KeyguardTransitionInteractor.class) ); biometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); biometricUnlockController.addListener(mBiometricUnlockEventsListener); @@ -462,6 +471,29 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void biometricUnlockStateResetOnTransitionFromGone() { + mBiometricUnlockController.consumeTransitionStepOnStartedKeyguardState( + new TransitionStep( + KeyguardState.AOD, + KeyguardState.GONE, + .1f /* value */, + TransitionState.STARTED + ) + ); + verify(mBiometricUnlockInteractor, never()).setBiometricUnlockState(anyInt()); + + mBiometricUnlockController.consumeTransitionStepOnStartedKeyguardState( + new TransitionStep( + KeyguardState.GONE, + KeyguardState.AOD, + .1f /* value */, + TransitionState.STARTED + ) + ); + verify(mBiometricUnlockInteractor).setBiometricUnlockState(eq(MODE_NONE)); + } + + @Test public void onFingerprintDetect_showBouncer() { // WHEN fingerprint detect occurs mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt index c4fc30de3d06..070a3697df68 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt @@ -19,7 +19,10 @@ package com.android.systemui.bouncer.domain.interactor import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.statusBarStateController @@ -37,6 +40,9 @@ var Kosmos.alternateBouncerInteractor by biometricSettingsRepository = biometricSettingsRepository, systemClock = systemClock, keyguardUpdateMonitor = keyguardUpdateMonitor, + deviceEntryFingerprintAuthInteractor = { deviceEntryFingerprintAuthInteractor }, + keyguardInteractor = { keyguardInteractor }, + keyguardTransitionInteractor = { keyguardTransitionInteractor }, scope = testScope.backgroundScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt index 5f5d42850619..7eef704c1622 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt @@ -28,6 +28,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository @@ -85,6 +86,9 @@ object KeyguardDismissInteractorFactory { FakeBiometricSettingsRepository(), FakeSystemClock(), keyguardUpdateMonitor, + { mock(DeviceEntryFingerprintAuthInteractor::class.java) }, + { mock(KeyguardInteractor::class.java) }, + { mock(KeyguardTransitionInteractor::class.java) }, testScope.backgroundScope, ) val powerInteractorWithDeps = diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 997f3af3533e..04b19ffd4dfc 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -24,10 +24,13 @@ flag { } flag { - name: "compute_window_changes_on_a11y" + name: "compute_window_changes_on_a11y_v2" namespace: "accessibility" description: "Computes accessibility window changes in accessibility instead of wm package." bug: "322444245" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index d30748478741..6007bfd99e7b 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -445,7 +445,7 @@ public class AccessibilityWindowManager { public void onWindowsForAccessibilityChanged(boolean forceSend, int topFocusedDisplayId, IBinder topFocusedWindowToken, @NonNull List<WindowInfo> windows) { synchronized (mLock) { - if (!Flags.computeWindowChangesOnA11y()) { + if (!Flags.computeWindowChangesOnA11yV2()) { // If the flag is enabled, it's already done in #createWindowInfoListLocked. updateWindowsByWindowAttributesLocked(windows); } @@ -491,7 +491,7 @@ public class AccessibilityWindowManager { /** * Called when the windows for accessibility changed. This is called if - * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y} is + * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2} is * true. * * @param forceSend Send the windows for accessibility even if they haven't @@ -996,7 +996,7 @@ public class AccessibilityWindowManager { final int windowId = findWindowIdLocked(userId, window.token); // With the flag enabled, createWindowInfoListLocked() already removes invalid windows. - if (!Flags.computeWindowChangesOnA11y()) { + if (!Flags.computeWindowChangesOnA11yV2()) { if (windowId < 0) { return null; } diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java deleted file mode 100644 index 0a4148535451..000000000000 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion; - -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.annotation.UserIdInt; -import android.companion.AssociationInfo; -import android.companion.CompanionDeviceService; -import android.companion.DevicePresenceEvent; -import android.content.ComponentName; -import android.content.Context; -import android.hardware.power.Mode; -import android.os.Handler; -import android.os.ParcelUuid; -import android.os.PowerManagerInternal; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.infra.PerUser; -import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; -import com.android.server.companion.presence.ObservableUuid; -import com.android.server.companion.presence.ObservableUuidStore; -import com.android.server.companion.utils.PackageUtils; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Manages communication with companion applications via - * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to - * the services, maintaining the connection (the binding), and invoking callback methods such as - * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, - * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and - * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the - * application process. - * - * <p> - * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be - * utilized by {@link CompanionDeviceManagerService}): - * <ul> - * <li> {@link #bindCompanionApplication(int, String, boolean)} - * <li> {@link #unbindCompanionApplication(int, String)} - * <li> {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} - * <li> {@link #isCompanionApplicationBound(int, String)} - * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} - * </ul> - * - * @see CompanionDeviceService - * @see android.companion.ICompanionDeviceService - * @see CompanionDeviceServiceConnector - */ -@SuppressLint("LongLogTag") -public class CompanionApplicationController { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionApplicationController"; - - private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec - - private final @NonNull Context mContext; - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final @NonNull CompanionServicesRegister mCompanionServicesRegister; - - private final PowerManagerInternal mPowerManagerInternal; - - @GuardedBy("mBoundCompanionApplications") - private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>> - mBoundCompanionApplications; - @GuardedBy("mScheduledForRebindingCompanionApplications") - private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; - - CompanionApplicationController(Context context, AssociationStore associationStore, - ObservableUuidStore observableUuidStore, - CompanionDevicePresenceMonitor companionDevicePresenceMonitor, - PowerManagerInternal powerManagerInternal) { - mContext = context; - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mDevicePresenceMonitor = companionDevicePresenceMonitor; - mPowerManagerInternal = powerManagerInternal; - mCompanionServicesRegister = new CompanionServicesRegister(); - mBoundCompanionApplications = new AndroidPackageMap<>(); - mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); - } - - void onPackagesChanged(@UserIdInt int userId) { - mCompanionServicesRegister.invalidate(userId); - } - - /** - * CDM binds to the companion app. - */ - public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, - boolean isSelfManaged) { - if (DEBUG) { - Log.i(TAG, "bind() u" + userId + "/" + packageName - + " isSelfManaged=" + isSelfManaged); - } - - final List<ComponentName> companionServices = - mCompanionServicesRegister.forPackage(userId, packageName); - if (companionServices.isEmpty()) { - Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " - + "eligible CompanionDeviceService not found.\n" - + "A CompanionDeviceService should declare an intent-filter for " - + "\"android.companion.CompanionDeviceService\" action and require " - + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); - return; - } - - final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>(); - synchronized (mBoundCompanionApplications) { - if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound."); - return; - } - - for (int i = 0; i < companionServices.size(); i++) { - boolean isPrimary = i == 0; - serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId, - companionServices.get(i), isSelfManaged, isPrimary)); - } - - mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors); - } - - // Set listeners for both Primary and Secondary connectors. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.setListener(this::onBinderDied); - } - - // Now "bind" all the connectors: the primary one and the rest of them. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.connect(); - } - } - - /** - * CDM unbinds the companion app. - */ - public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { - if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName); - - final List<CompanionDeviceServiceConnector> serviceConnectors; - - synchronized (mBoundCompanionApplications) { - serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - - if (serviceConnectors == null) { - if (DEBUG) { - Log.e(TAG, "unbindCompanionApplication(): " - + "u" + userId + "/" + packageName + " is NOT bound"); - Log.d(TAG, "Stacktrace", new Throwable()); - } - return; - } - - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.postUnbind(); - } - } - - /** - * @return whether the companion application is bound now. - */ - public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { - synchronized (mBoundCompanionApplications) { - return mBoundCompanionApplications.containsValueForPackage(userId, packageName); - } - } - - private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, - CompanionDeviceServiceConnector serviceConnector) { - Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); - - if (isRebindingCompanionApplicationScheduled(userId, packageName)) { - if (DEBUG) { - Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " - + serviceConnector.getComponentName()); - } - return; - } - - if (serviceConnector.isPrimary()) { - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.setValueForPackage( - userId, packageName, true); - } - } - - // Rebinding in 10 seconds. - Handler.getMain().postDelayed(() -> - onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector), - REBIND_TIMEOUT); - } - - private boolean isRebindingCompanionApplicationScheduled( - @UserIdInt int userId, @NonNull String packageName) { - synchronized (mScheduledForRebindingCompanionApplications) { - return mScheduledForRebindingCompanionApplications.containsValueForPackage( - userId, packageName); - } - } - - private void onRebindingCompanionApplicationTimeout( - @UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - // Re-mark the application is bound. - if (serviceConnector.isPrimary()) { - synchronized (mBoundCompanionApplications) { - if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - List<CompanionDeviceServiceConnector> serviceConnectors = - Collections.singletonList(serviceConnector); - mBoundCompanionApplications.setValueForPackage(userId, packageName, - serviceConnectors); - } - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - } - - serviceConnector.connect(); - } - - /** - * Notify the app that the device appeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Appeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceAppeared(association); - } - - /** - * Notify the app that the device disappeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceDisappeared(association); - } - - /** - * Notify the app that the device appeared. - */ - public void notifyCompanionDevicePresenceEvent(AssociationInfo association, int event) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(association.getId(), event, null); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() - + "] event=[" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - /** - * Notify the app that the device disappeared. - */ - public void notifyUuidDevicePresenceEvent(ObservableUuid uuid, int event) { - final int userId = uuid.getUserId(); - final ParcelUuid parcelUuid = uuid.getUuid(); - final String packageName = uuid.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "]" + "event= [" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - void dump(@NonNull PrintWriter out) { - out.append("Companion Device Application Controller: \n"); - - synchronized (mBoundCompanionApplications) { - out.append(" Bound Companion Applications: "); - if (mBoundCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mBoundCompanionApplications.dump(out); - } - } - - out.append(" Companion Applications Scheduled For Rebinding: "); - if (mScheduledForRebindingCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mScheduledForRebindingCompanionApplications.dump(out); - } - } - - /** - * Rebinding for Self-Managed secondary services OR Non-Self-Managed services. - */ - private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - - boolean isPrimary = serviceConnector.isPrimary(); - Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); - - // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. - if (isPrimary) { - final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - - for (AssociationInfo association : associations) { - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - break; - } - } - - synchronized (mBoundCompanionApplications) { - mBoundCompanionApplications.removePackage(userId, packageName); - } - } - - // Second: schedule rebinding if needed. - final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); - - if (shouldScheduleRebind) { - scheduleRebinding(userId, packageName, serviceConnector); - } - } - - private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector( - @UserIdInt int userId, @NonNull String packageName) { - final List<CompanionDeviceServiceConnector> connectors; - synchronized (mBoundCompanionApplications) { - connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName); - } - return connectors != null ? connectors.get(0) : null; - } - - private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { - // Make sure do not schedule rebind for the case ServiceConnector still gets callback after - // app is uninstalled. - boolean stillAssociated = false; - // Make sure to clean up the state for all the associations - // that associate with this package. - boolean shouldScheduleRebind = false; - boolean shouldScheduleRebindForUuid = false; - final List<ObservableUuid> uuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo ai : - mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { - final int associationId = ai.getId(); - stillAssociated = true; - if (ai.isSelfManaged()) { - // Do not rebind if primary one is died for selfManaged application. - if (isPrimary - && mDevicePresenceMonitor.isDevicePresent(associationId)) { - mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId); - shouldScheduleRebind = false; - } - // Do not rebind if both primary and secondary services are died for - // selfManaged application. - shouldScheduleRebind = isCompanionApplicationBound(userId, packageName); - } else if (ai.isNotifyOnDeviceNearby()) { - // Always rebind for non-selfManaged devices. - shouldScheduleRebind = true; - } - } - - for (ObservableUuid uuid : uuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - shouldScheduleRebindForUuid = true; - break; - } - } - - return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; - } - - private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { - @Override - public synchronized @NonNull Map<String, List<ComponentName>> forUser( - @UserIdInt int userId) { - return super.forUser(userId); - } - - synchronized @NonNull List<ComponentName> forPackage( - @UserIdInt int userId, @NonNull String packageName) { - return forUser(userId).getOrDefault(packageName, Collections.emptyList()); - } - - synchronized void invalidate(@UserIdInt int userId) { - remove(userId); - } - - @Override - protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { - return PackageUtils.getCompanionServicesForUser(mContext, userId); - } - } - - /** - * Associates an Android package (defined by userId + packageName) with a value of type T. - */ - private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> { - - void setValueForPackage( - @UserIdInt int userId, @NonNull String packageName, @NonNull T value) { - Map<String, T> forUser = get(userId); - if (forUser == null) { - forUser = /* Map<String, T> */ new HashMap(); - put(userId, forUser); - } - - forUser.put(packageName, value); - } - - boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, ?> forUser = get(userId); - return forUser != null && forUser.containsKey(packageName); - } - - T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - return forUser != null ? forUser.get(packageName) : null; - } - - T removePackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - if (forUser == null) return null; - return forUser.remove(packageName); - } - - void dump() { - if (size() == 0) { - Log.d(TAG, "<empty>"); - return; - } - - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - Log.d(TAG, "u" + userId + ": <empty>"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value); - } - } - } - - private void dump(@NonNull PrintWriter out) { - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - out.append(" u").append(String.valueOf(userId)).append(": <empty>\n"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - out.append(" u").append(String.valueOf(userId)).append("\\") - .append(packageName).append(" -> ") - .append(value.toString()).append('\n'); - } - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 712162b2d3b5..edf9fd13d51f 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -20,15 +20,10 @@ package com.android.server.companion; import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; +import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE; import static android.Manifest.permission.USE_COMPANION_TRANSPORTS; -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -42,13 +37,10 @@ import static com.android.server.companion.utils.PackageUtils.getPackageInfo; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; -import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; import android.annotation.EnforcePermission; @@ -64,7 +56,6 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.companion.AssociationInfo; import android.companion.AssociationRequest; -import android.companion.DeviceNotAssociatedException; import android.companion.IAssociationRequestCallback; import android.companion.ICompanionDeviceManager; import android.companion.IOnAssociationsChangedListener; @@ -79,7 +70,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; -import android.hardware.power.Mode; import android.net.MacAddress; import android.net.NetworkPolicyManager; import android.os.Binder; @@ -91,7 +81,6 @@ import android.os.PowerExemptionManager; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; @@ -118,7 +107,8 @@ import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.presence.ObservableUuidStore; import com.android.server.companion.transport.CompanionTransportManager; @@ -131,10 +121,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; @SuppressLint("LongLogTag") @@ -146,10 +133,6 @@ public class CompanionDeviceManagerService extends SystemService { private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; - private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = - "debug.cdm.cdmservice.removal_time_window"; - - private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); private static final int MAX_CN_LENGTH = 500; private final ActivityTaskManagerInternal mAtmInternal; @@ -165,8 +148,8 @@ public class CompanionDeviceManagerService extends SystemService { private final AssociationRequestsProcessor mAssociationRequestsProcessor; private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final BackupRestoreProcessor mBackupRestoreProcessor; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final CompanionApplicationController mCompanionAppController; + private final DevicePresenceProcessor mDevicePresenceProcessor; + private final CompanionAppBinder mCompanionAppBinder; private final CompanionTransportManager mTransportManager; private final DisassociationProcessor mDisassociationProcessor; private final CrossDeviceSyncController mCrossDeviceSyncController; @@ -185,7 +168,7 @@ public class CompanionDeviceManagerService extends SystemService { mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); final AssociationDiskStore associationDiskStore = new AssociationDiskStore(); - mAssociationStore = new AssociationStore(userManager, associationDiskStore); + mAssociationStore = new AssociationStore(context, userManager, associationDiskStore); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); mObservableUuidStore = new ObservableUuidStore(); @@ -196,18 +179,17 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); - mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager, - mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); + mCompanionAppBinder = new CompanionAppBinder(context); - mCompanionAppController = new CompanionApplicationController( - context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor, + mDevicePresenceProcessor = new DevicePresenceProcessor(context, + mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); mTransportManager = new CompanionTransportManager(context, mAssociationStore); mDisassociationProcessor = new DisassociationProcessor(context, activityManager, - mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor, - mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager); + mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor, + mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, @@ -242,7 +224,7 @@ public class CompanionDeviceManagerService extends SystemService { // delays (even in case of the Main Thread). It may be fine overall, but would require // updating the tests (adding a delay there). mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true); - mDevicePresenceMonitor.init(context); + mDevicePresenceProcessor.init(context); } else if (phase == PHASE_BOOT_COMPLETED) { // Run the Inactive Association Removal job service daily. InactiveAssociationsRemovalService.schedule(getContext()); @@ -271,7 +253,7 @@ public class CompanionDeviceManagerService extends SystemService { // Notify and bind the app after the phone is unlocked. final int userId = user.getUserIdentifier(); final Set<BluetoothDevice> blueToothDevices = - mDevicePresenceMonitor.getPendingConnectedDevices().get(userId); + mDevicePresenceProcessor.getPendingConnectedDevices().get(userId); final List<ObservableUuid> observableUuids = mObservableUuidStore.getObservableUuidsForUser(userId); @@ -287,14 +269,14 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore.getActiveAssociationsByAddress( bluetoothDevice.getAddress())) { Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected"); - mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId()); + mDevicePresenceProcessor.onBluetoothCompanionDeviceConnected(ai.getId()); } for (ObservableUuid observableUuid : observableUuids) { if (deviceUuids.contains(observableUuid.getUuid())) { Slog.i(TAG, "onUserUnlocked, UUID( " + observableUuid.getUuid() + " ) is connected"); - mDevicePresenceMonitor.onDevicePresenceEventByUuid( + mDevicePresenceProcessor.onDevicePresenceEventByUuid( observableUuid, EVENT_BT_CONNECTED); } } @@ -302,181 +284,6 @@ public class CompanionDeviceManagerService extends SystemService { } } - @NonNull - AssociationInfo getAssociationWithCallerChecks( - @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, macAddress); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - @NonNull - AssociationInfo getAssociationWithCallerChecks(int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - private void onDeviceAppearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association); - } - - private void onDeviceDisappearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association); - } - } - - private void onDevicePresenceEventInternal(int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event); - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - switch (event) { - case EVENT_BLE_APPEARED: - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - } - // Check if there are other devices associated to the app that are present. - if (shouldBindPackage(userId, packageName)) return; - mCompanionAppController.unbindCompanionApplication(userId, packageName); - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) { - Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid() - + "for package=" + uuid.getPackageName() + " event=" + event); - final String packageName = uuid.getPackageName(); - final int userId = uuid.getUserId(); - - switch (event) { - case EVENT_BT_CONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, /*bindImportant*/ false); - - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - - break; - case EVENT_BT_DISCONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - // Check if there are other devices associated to the app or the UUID to be - // observed are present. - if (shouldBindPackage(userId, packageName)) return; - - mCompanionAppController.unbindCompanionApplication(userId, packageName); - - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void bindApplicationIfNeeded(AssociationInfo association) { - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - // Set bindImportant to true when the association is self-managed to avoid the target - // service being killed. - final boolean bindImportant = association.isSelfManaged(); - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, bindImportant); - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - } - - /** - * @return whether the package should be bound (i.e. at least one of the devices associated with - * the package is currently present OR the UUID to be observed by this package is - * currently present). - */ - private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { - final List<AssociationInfo> packageAssociations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo association : packageAssociations) { - if (!association.shouldBindWhenPresent()) continue; - if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true; - } - - for (ObservableUuid uuid : observableUuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - return true; - } - } - - return false; - } - private void onPackageRemoveOrDataClearedInternal( @UserIdInt int userId, @NonNull String packageName) { if (DEBUG) { @@ -502,7 +309,7 @@ public class CompanionDeviceManagerService extends SystemService { mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName); } - mCompanionAppController.onPackagesChanged(userId); + mCompanionAppBinder.onPackagesChanged(userId); } private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) { @@ -515,34 +322,15 @@ public class CompanionDeviceManagerService extends SystemService { association.getPackageName()); } - mCompanionAppController.onPackagesChanged(userId); + mCompanionAppBinder.onPackagesChanged(userId); } private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { mBackupRestoreProcessor.restorePendingAssociations(userId, packageName); } - // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { - final long currentTime = System.currentTimeMillis(); - long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); - if (removalWindow <= 0) { - // 0 or negative values indicate that the sysprop was never set or should be ignored. - removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; - } - - for (AssociationInfo association : mAssociationStore.getAssociations()) { - if (!association.isSelfManaged()) continue; - - final boolean isInactive = - currentTime - association.getLastTimeConnectedMs() >= removalWindow; - if (!isInactive) continue; - - final int id = association.getId(); - - Slog.i(TAG, "Removing inactive self-managed association id=" + id); - mDisassociationProcessor.disassociate(id); - } + mDisassociationProcessor.removeIdleSelfManagedAssociations(); } public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { @@ -679,24 +467,15 @@ public class CompanionDeviceManagerService extends SystemService { @Deprecated @Override public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) { - Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName - + ", macAddress=" + deviceMacAddress); - requireNonNull(deviceMacAddress); requireNonNull(packageName); - final AssociationInfo association = - getAssociationWithCallerChecks(userId, packageName, deviceMacAddress); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress); } @Override public void disassociate(int associationId) { - Slog.i(TAG, "disassociate() associationId=" + associationId); - - final AssociationInfo association = - getAssociationWithCallerChecks(associationId); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(associationId); } @Override @@ -758,21 +537,25 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void registerDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - registerDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, true); + public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStartObservingDevicePresence_enforcePermission(); + + mDevicePresenceProcessor.startObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void unregisterDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - unregisterDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, false); + public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStopObservingDevicePresence_enforcePermission(); + + mDevicePresenceProcessor.stopObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override @@ -780,7 +563,8 @@ public class CompanionDeviceManagerService extends SystemService { public void startObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { startObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ true); + + mDevicePresenceProcessor.startObservingDevicePresence(request, packageName, userId); } @Override @@ -788,80 +572,8 @@ public class CompanionDeviceManagerService extends SystemService { public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { stopObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ false); - } - - private void registerDevicePresenceListener(ObservingDevicePresenceRequest request, - String packageName, int userId, boolean active) { - enforceUsesCompanionDeviceFeature(getContext(), userId, packageName); - enforceCallerIsSystemOr(userId, packageName); - - final int associationId = request.getAssociationId(); - final AssociationInfo associationInfo = mAssociationStore.getAssociationById( - associationId); - final ParcelUuid uuid = request.getUuid(); - - if (uuid != null) { - enforceCallerCanObservingDevicePresenceByUuid(getContext()); - if (active) { - startObservingDevicePresenceByUuid(uuid, packageName, userId); - } else { - stopObservingDevicePresenceByUuid(uuid, packageName, userId); - } - } else if (associationInfo == null) { - throw new IllegalArgumentException("App " + packageName - + " is not associated with device " + request.getAssociationId() - + " for user " + userId); - } else { - processDevicePresenceListener( - associationInfo, userId, packageName, active); - } - } - - private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (ObservableUuid observableUuid : observableUuids) { - if (observableUuid.getUuid().equals(uuid)) { - Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName - + "has been already scheduled for observing"); - return; - } - } - - final ObservableUuid observableUuid = new ObservableUuid(userId, uuid, - packageName, System.currentTimeMillis()); - - mObservableUuidStore.writeObservableUuid(userId, observableUuid); - } - - private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> uuidsTobeObserved = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - boolean isScheduledObserving = false; - - for (ObservableUuid observableUuid : uuidsTobeObserved) { - if (observableUuid.getUuid().equals(uuid)) { - isScheduledObserving = true; - break; - } - } - if (!isScheduledObserving) { - Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName - + "has NOT been scheduled for observing yet"); - return; - } - - mObservableUuidStore.removeObservableUuid(userId, uuid, packageName); - mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid); - - if (!shouldBindPackage(userId, packageName)) { - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } + mDevicePresenceProcessor.stopObservingDevicePresence(request, packageName, userId); } @Override @@ -874,8 +586,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public boolean isPermissionTransferUserConsented(String packageName, int userId, int associationId) { - return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName, - userId, associationId); + return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId); } @Override @@ -891,8 +602,7 @@ public class CompanionDeviceManagerService extends SystemService { ParcelFileDescriptor fd) { attachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd); + mTransportManager.attachSystemDataTransport(associationId, fd); } @Override @@ -900,161 +610,61 @@ public class CompanionDeviceManagerService extends SystemService { public void detachSystemDataTransport(String packageName, int userId, int associationId) { detachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.detachSystemDataTransport(packageName, userId, associationId); + mTransportManager.detachSystemDataTransport(associationId); + } + + @Override + @EnforcePermission(MANAGE_COMPANION_DEVICES) + public void enableSecureTransport(boolean enabled) { + enableSecureTransport_enforcePermission(); + + mTransportManager.enableSecureTransport(enabled); } @Override public void enableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags); } @Override public void disableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags); } @Override public void enablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.enablePermissionsSync(associationId); } @Override public void disablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.disablePermissionsSync(associationId); } @Override public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - // TODO: temporary fix, will remove soon - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (association == null) { - return null; - } - getAssociationWithCallerChecks(associationId); return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId); } @Override - @EnforcePermission(MANAGE_COMPANION_DEVICES) - public void enableSecureTransport(boolean enabled) { - enableSecureTransport_enforcePermission(); - mTransportManager.enableSecureTransport(enabled); - } + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceAppeared(int associationId) { + notifySelfManagedDeviceAppeared_enforcePermission(); - @Override - public void notifyDeviceAppeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId); - - AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // timestamp. - association = (new AssociationInfo.Builder(association)) - .setLastTimeConnected(System.currentTimeMillis()) - .build(); - mAssociationStore.updateAssociation(association); - - mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId); - - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true); - } + mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, true); } @Override - public void notifyDeviceDisappeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId); - - final AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - - mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId); + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceDisappeared(int associationId) { + notifySelfManagedDeviceDisappeared_enforcePermission(); - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - } + mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, false); } @Override public boolean isCompanionApplicationBound(String packageName, int userId) { - return mCompanionAppController.isCompanionApplicationBound(userId, packageName); - } - - private void registerDevicePresenceListenerActive(String packageName, String deviceAddress, - boolean active) throws RemoteException { - if (DEBUG) { - Log.i(TAG, "registerDevicePresenceListenerActive()" - + " active=" + active - + " deviceAddress=" + deviceAddress); - } - final int userId = getCallingUserId(); - enforceCallerIsSystemOr(userId, packageName); - - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, deviceAddress); - - if (association == null) { - throw new RemoteException(new DeviceNotAssociatedException("App " + packageName - + " is not associated with device " + deviceAddress - + " for user " + userId)); - } - - processDevicePresenceListener(association, userId, packageName, active); - } - - private void processDevicePresenceListener(AssociationInfo association, - int userId, String packageName, boolean active) { - // If already at specified state, then no-op. - if (active == association.isNotifyOnDeviceNearby()) { - if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state."); - return; - } - - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // flag. - association = (new AssociationInfo.Builder(association)) - .setNotifyOnDeviceNearby(active) - .build(); - // Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will - // trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when - // an application sets/unsets the mNotifyOnDeviceNearby flag. - mAssociationStore.updateAssociation(association); - - int associationId = association.getId(); - // If device is already present, then trigger callback. - if (active && mDevicePresenceMonitor.isDevicePresent(associationId)) { - Slog.i(TAG, "Device is already present. Triggering callback."); - if (mDevicePresenceMonitor.isBlePresent(associationId) - || mDevicePresenceMonitor.isSimulatePresent(associationId)) { - onDeviceAppearedInternal(associationId); - onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED); - } else if (mDevicePresenceMonitor.isBtConnected(associationId)) { - onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED); - } - } - - // If last listener is unregistered, then unbind application. - if (!active && !shouldBindPackage(userId, packageName)) { - if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application."); - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } + return mCompanionAppBinder.isCompanionApplicationBound(userId, packageName); } @Override @@ -1070,7 +680,8 @@ public class CompanionDeviceManagerService extends SystemService { } final MacAddress macAddressObj = MacAddress.fromString(macAddress); - createNewAssociation(userId, packageName, macAddressObj, null, null, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, + null, null, null, false, null, null); } private void checkCanCallNotificationApi(String callingPackage, int userId) { @@ -1099,9 +710,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void setAssociationTag(int associationId, String tag) { - AssociationInfo association = getAssociationWithCallerChecks(associationId); - association = (new AssociationInfo.Builder(association)).setTag(tag).build(); - mAssociationStore.updateAssociation(association); + mAssociationRequestsProcessor.setAssociationTag(associationId, tag); } @Override @@ -1124,7 +733,7 @@ public class CompanionDeviceManagerService extends SystemService { @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, @NonNull String[] args) { return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this, - mAssociationStore, mDevicePresenceMonitor, mTransportManager, + mAssociationStore, mDevicePresenceProcessor, mTransportManager, mSystemDataTransferProcessor, mAssociationRequestsProcessor, mBackupRestoreProcessor, mDisassociationProcessor) .exec(this, in.getFileDescriptor(), out.getFileDescriptor(), @@ -1139,21 +748,13 @@ public class CompanionDeviceManagerService extends SystemService { } mAssociationStore.dump(out); - mDevicePresenceMonitor.dump(out); - mCompanionAppController.dump(out); + mDevicePresenceProcessor.dump(out); + mCompanionAppBinder.dump(out); mTransportManager.dump(out); mSystemDataTransferRequestStore.dump(out); } } - void createNewAssociation(@UserIdInt int userId, @NonNull String packageName, - @Nullable MacAddress macAddress, @Nullable CharSequence displayName, - @Nullable String deviceProfile, boolean isSelfManaged) { - mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged, - /* callback */ null, /* resultReceiver */ null); - } - /** * Update special access for the association's package */ @@ -1169,8 +770,6 @@ public class CompanionDeviceManagerService extends SystemService { return; } - Slog.i(TAG, "Updating special access for package=[" + packageInfo.packageName + "]..."); - if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { @@ -1280,29 +879,6 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback = - new CompanionDevicePresenceMonitor.Callback() { - @Override - public void onDeviceAppeared(int associationId) { - onDeviceAppearedInternal(associationId); - } - - @Override - public void onDeviceDisappeared(int associationId) { - onDeviceDisappearedInternal(associationId); - } - - @Override - public void onDevicePresenceEvent(int associationId, int event) { - onDevicePresenceEventInternal(associationId, event); - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - onDevicePresenceEventByUuidInternal(uuid, event); - } - }; - private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { @@ -1315,7 +891,7 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void onPackageModified(String packageName) { + public void onPackageModified(@NonNull String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } @@ -1325,25 +901,15 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) { - final Map<String, Set<Integer>> copy = new HashMap<>(); - - for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) { - final Set<Integer> valueCopy = new HashSet<>(entry.getValue()); - copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy)); - } - - return Collections.unmodifiableMap(copy); - } - private static <T> boolean containsEither(T[] array, T a, T b) { return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); } private class LocalService implements CompanionDeviceManagerServiceInternal { + @Override public void removeInactiveSelfManagedAssociations() { - CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations(); + mDisassociationProcessor.removeIdleSelfManagedAssociations(); } @Override diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index cdf832f8c788..9d1250d361c4 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -27,8 +27,9 @@ import java.util.Collection; * Companion Device Manager Local System Service Interface. */ public interface CompanionDeviceManagerServiceInternal { + /** - * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations + * Remove idle self-managed associations. */ void removeInactiveSelfManagedAssociations(); diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index a7a73cb6bddb..a78938400a1e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,8 +18,6 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; - import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -38,7 +36,7 @@ import com.android.server.companion.association.DisassociationProcessor; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.transport.CompanionTransportManager; @@ -51,7 +49,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final CompanionDeviceManagerService mService; private final DisassociationProcessor mDisassociationProcessor; private final AssociationStore mAssociationStore; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceProcessor; private final CompanionTransportManager mTransportManager; private final SystemDataTransferProcessor mSystemDataTransferProcessor; @@ -60,7 +58,7 @@ class CompanionDeviceShellCommand extends ShellCommand { CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStore associationStore, - CompanionDevicePresenceMonitor devicePresenceMonitor, + DevicePresenceProcessor devicePresenceProcessor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor, @@ -68,7 +66,7 @@ class CompanionDeviceShellCommand extends ShellCommand { DisassociationProcessor disassociationProcessor) { mService = service; mAssociationStore = associationStore; - mDevicePresenceMonitor = devicePresenceMonitor; + mDevicePresenceProcessor = devicePresenceProcessor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; @@ -85,7 +83,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) { associationId = getNextIntArgRequired(); int event = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, event); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, event); return 0; } @@ -97,7 +95,7 @@ class CompanionDeviceShellCommand extends ShellCommand { ObservableUuid observableUuid = new ObservableUuid( userId, ParcelUuid.fromString(uuid), packageName, System.currentTimeMillis()); - mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event); + mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event); return 0; } @@ -124,8 +122,9 @@ class CompanionDeviceShellCommand extends ShellCommand { String address = getNextArgRequired(); String deviceProfile = getNextArg(); final MacAddress macAddress = MacAddress.fromString(address); - mService.createNewAssociation(userId, packageName, macAddress, - /* displayName= */ deviceProfile, deviceProfile, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, + deviceProfile, deviceProfile, /* associatedDevice */ null, false, + /* callback */ null, /* resultReceiver */ null); } break; @@ -134,8 +133,13 @@ class CompanionDeviceShellCommand extends ShellCommand { final String packageName = getNextArgRequired(); final String address = getNextArgRequired(); final AssociationInfo association = - mService.getAssociationWithCallerChecks(userId, packageName, address); - mDisassociationProcessor.disassociate(association.getId()); + mAssociationStore.getFirstAssociationByAddress(userId, packageName, + address); + if (association == null) { + out.println("Association doesn't exist."); + } else { + mDisassociationProcessor.disassociate(association.getId()); + } } break; @@ -144,9 +148,7 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { - mDisassociationProcessor.disassociate(association.getId()); - } + mDisassociationProcessor.disassociate(association.getId()); } } break; @@ -157,12 +159,12 @@ class CompanionDeviceShellCommand extends ShellCommand { case "simulate-device-appeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0); break; case "simulate-device-disappeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1); break; case "get-backup-payload": { @@ -410,10 +412,9 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Remove an existing Association."); pw.println(" disassociate-all USER_ID"); pw.println(" Remove all Associations for a user."); - pw.println(" clear-association-memory-cache"); + pw.println(" refresh-cache"); pw.println(" Clear the in-memory association cache and reload all association "); - pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); - pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); pw.println(" simulate-device-appeared ASSOCIATION_ID"); pw.println(" Make CDM act as if the given companion device has appeared."); diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index a02d9f912bcd..a18776e67200 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -145,7 +145,8 @@ public class AssociationRequestsProcessor { /** * Handle incoming {@link AssociationRequest}s, sent via - * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)} + * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, + * IAssociationRequestCallback, String, int)} */ public void processNewAssociationRequest(@NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @@ -212,7 +213,8 @@ public class AssociationRequestsProcessor { // 2b.4. Send the PendingIntent back to the app. try { callback.onAssociationPending(pendingIntent); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } } /** @@ -252,7 +254,8 @@ public class AssociationRequestsProcessor { // forward it back to the application via the callback. try { callback.onFailure(e.getMessage()); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } return; } @@ -322,7 +325,8 @@ public class AssociationRequestsProcessor { * Enable system data sync. */ public void enableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build(); mAssociationStore.updateAssociation(updated); @@ -332,12 +336,23 @@ public class AssociationRequestsProcessor { * Disable system data sync. */ public void disableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build(); mAssociationStore.updateAssociation(updated); } + /** + * Set association tag. + */ + public void setAssociationTag(int associationId, String tag) { + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + association = (new AssociationInfo.Builder(association)).setTag(tag).build(); + mAssociationStore.updateAssociation(association); + } + private void sendCallbackAndFinish(@Nullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { @@ -396,14 +411,14 @@ public class AssociationRequestsProcessor { // If the application already has a pending association request, that PendingIntent // will be cancelled except application wants to cancel the request by the system. return Binder.withCleanCallingIdentity(() -> - PendingIntent.getActivityAsUser( - mContext, /*requestCode */ packageUid, intent, - FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, - ActivityOptions.makeBasic() - .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - .toBundle(), - UserHandle.CURRENT) + PendingIntent.getActivityAsUser( + mContext, /*requestCode */ packageUid, intent, + FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(), + UserHandle.CURRENT) ); } diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index edebb55233d0..ae2b70852a35 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -18,6 +18,7 @@ package com.android.server.companion.association; import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation; +import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage; import android.annotation.IntDef; import android.annotation.NonNull; @@ -26,6 +27,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.IOnAssociationsChangedListener; +import android.content.Context; import android.content.pm.UserInfo; import android.net.MacAddress; import android.os.Binder; @@ -57,21 +59,22 @@ import java.util.concurrent.Executors; @SuppressLint("LongLogTag") public class AssociationStore { - @IntDef(prefix = { "CHANGE_TYPE_" }, value = { + @IntDef(prefix = {"CHANGE_TYPE_"}, value = { CHANGE_TYPE_ADDED, CHANGE_TYPE_REMOVED, CHANGE_TYPE_UPDATED_ADDRESS_CHANGED, CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, }) @Retention(RetentionPolicy.SOURCE) - public @interface ChangeType {} + public @interface ChangeType { + } public static final int CHANGE_TYPE_ADDED = 0; public static final int CHANGE_TYPE_REMOVED = 1; public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2; public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3; - /** Listener for any changes to associations. */ + /** Listener for any changes to associations. */ public interface OnChangeListener { /** * Called when there are association changes. @@ -100,25 +103,30 @@ public class AssociationStore { /** * Called when an association is added. */ - default void onAssociationAdded(AssociationInfo association) {} + default void onAssociationAdded(AssociationInfo association) { + } /** * Called when an association is removed. */ - default void onAssociationRemoved(AssociationInfo association) {} + default void onAssociationRemoved(AssociationInfo association) { + } /** * Called when an association is updated. */ - default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {} + default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) { + } } private static final String TAG = "CDM_AssociationStore"; - private final Object mLock = new Object(); - + private final Context mContext; + private final UserManager mUserManager; + private final AssociationDiskStore mDiskStore; private final ExecutorService mExecutor; + private final Object mLock = new Object(); @GuardedBy("mLock") private boolean mPersisted = false; @GuardedBy("mLock") @@ -132,10 +140,9 @@ public class AssociationStore { private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners = new RemoteCallbackList<>(); - private final UserManager mUserManager; - private final AssociationDiskStore mDiskStore; - - public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) { + public AssociationStore(Context context, UserManager userManager, + AssociationDiskStore diskStore) { + mContext = context; mUserManager = userManager; mDiskStore = diskStore; mExecutor = Executors.newSingleThreadExecutor(); @@ -202,7 +209,7 @@ public class AssociationStore { synchronized (mLock) { if (mIdToAssociationMap.containsKey(id)) { - Slog.e(TAG, "Association with id=[" + id + "] already exists."); + Slog.e(TAG, "Association id=[" + id + "] already exists."); return; } @@ -449,6 +456,26 @@ public class AssociationStore { } /** + * Get association by id with caller checks. + */ + @NonNull + public AssociationInfo getAssociationWithCallerChecks(int associationId) { + AssociationInfo association = getAssociationById(associationId); + if (association == null) { + throw new IllegalArgumentException( + "getAssociationWithCallerChecks() Association id=[" + associationId + + "] doesn't exist."); + } + if (checkCallerCanManageAssociationsForPackage(mContext, association.getUserId(), + association.getPackageName())) { + return association; + } + + throw new IllegalArgumentException( + "The caller can't interact with the association id=[" + associationId + "]."); + } + + /** * Register a local listener for association changes. */ public void registerLocalListener(@NonNull OnChangeListener listener) { diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java index ec8977918c56..acf683d387a3 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -22,6 +22,8 @@ import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PRO import static com.android.internal.util.CollectionUtils.any; import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation; +import static java.util.concurrent.TimeUnit.DAYS; + import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.UserIdInt; @@ -30,21 +32,27 @@ import android.companion.AssociationInfo; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.os.Binder; +import android.os.SystemProperties; import android.os.UserHandle; import android.util.Slog; -import com.android.server.companion.CompanionApplicationController; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.transport.CompanionTransportManager; /** - * A class response for Association removal. + * This class responsible for disassociation. */ @SuppressLint("LongLogTag") public class DisassociationProcessor { private static final String TAG = "CDM_DisassociationProcessor"; + + private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = + "debug.cdm.cdmservice.removal_time_window"; + private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); + @NonNull private final Context mContext; @NonNull @@ -52,11 +60,11 @@ public class DisassociationProcessor { @NonNull private final PackageManagerInternal mPackageManagerInternal; @NonNull - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceMonitor; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull - private final CompanionApplicationController mCompanionAppController; + private final CompanionAppBinder mCompanionAppController; @NonNull private final CompanionTransportManager mTransportManager; private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; @@ -66,8 +74,8 @@ public class DisassociationProcessor { @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, - @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, - @NonNull CompanionApplicationController applicationController, + @NonNull DevicePresenceProcessor devicePresenceMonitor, + @NonNull CompanionAppBinder applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager) { mContext = context; @@ -89,11 +97,7 @@ public class DisassociationProcessor { public void disassociate(int id) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); - final AssociationInfo association = mAssociationStore.getAssociationById(id); - if (association == null) { - Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist."); - return; - } + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); final int userId = association.getUserId(); final String packageName = association.getPackageName(); @@ -118,12 +122,12 @@ public class DisassociationProcessor { return; } + // Detach transport if exists + mTransportManager.detachSystemDataTransport(id); + // Association cleanup. - mAssociationStore.removeAssociation(association.getId()); mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - - // Detach transport if exists - mTransportManager.detachSystemDataTransport(packageName, userId, id); + mAssociationStore.removeAssociation(association.getId()); // If role is not in use by other associations, revoke the role. // Do not need to remove the system role since it was pre-granted by the system. @@ -143,10 +147,28 @@ public class DisassociationProcessor { it -> it.isNotifyOnDeviceNearby() && mDevicePresenceMonitor.isDevicePresent(it.getId())); if (!shouldStayBound) { - mCompanionAppController.unbindCompanionApplication(userId, packageName); + mCompanionAppController.unbindCompanionApp(userId, packageName); } } + /** + * @deprecated Use {@link #disassociate(int)} instead. + */ + @Deprecated + public void disassociate(int userId, String packageName, String macAddress) { + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, macAddress); + + if (association == null) { + throw new IllegalArgumentException( + "Association for mac address=[" + macAddress + "] doesn't exist"); + } + + mAssociationStore.getAssociationWithCallerChecks(association.getId()); + + disassociate(association.getId()); + } + @SuppressLint("MissingPermission") private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { return Binder.withCleanCallingIdentity(() -> { @@ -163,7 +185,7 @@ public class DisassociationProcessor { () -> mActivityManager.addOnUidImportanceListener( mOnPackageVisibilityChangeListener, ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { Slog.e(TAG, "Failed to start listening to uid importance changes."); } } @@ -179,6 +201,34 @@ public class DisassociationProcessor { } /** + * Remove idle self-managed associations. + */ + public void removeIdleSelfManagedAssociations() { + Slog.i(TAG, "Removing idle self-managed associations."); + + final long currentTime = System.currentTimeMillis(); + long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); + if (removalWindow <= 0) { + // 0 or negative values indicate that the sysprop was never set or should be ignored. + removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; + } + + for (AssociationInfo association : mAssociationStore.getAssociations()) { + if (!association.isSelfManaged()) continue; + + final boolean isInactive = + currentTime - association.getLastTimeConnectedMs() >= removalWindow; + if (!isInactive) continue; + + final int id = association.getId(); + + Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString() + + "]."); + disassociate(id); + } + } + + /** * An OnUidImportanceListener class which watches the importance of the packages. * In this class, we ONLY interested in the importance of the running process is greater than * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}. diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java index f28731548dcc..b509e71626ea 100644 --- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -30,7 +30,7 @@ import com.android.server.LocalServices; import com.android.server.companion.CompanionDeviceManagerServiceInternal; /** - * A Job Service responsible for clean up idle self-managed associations. + * A Job Service responsible for clean up self-managed associations if it's idle for 90 days. * * The job will be executed only if the device is charging and in idle mode due to the application * will be killed if association/role are revoked. See {@link DisassociationProcessor} @@ -45,10 +45,10 @@ public class InactiveAssociationsRemovalService extends JobService { @Override public boolean onStartJob(final JobParameters params) { Slog.i(TAG, "Execute the Association Removal job"); - // Special policy for selfManaged that need to revoke associations if the device - // does not connect for 90 days. + LocalServices.getService(CompanionDeviceManagerServiceInternal.class) .removeInactiveSelfManagedAssociations(); + jobFinished(params, false); return true; } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index c5ca0bf7e9c5..9069689ee5eb 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -31,7 +31,6 @@ import android.annotation.UserIdInt; import android.app.ActivityOptions; import android.app.PendingIntent; import android.companion.AssociationInfo; -import android.companion.DeviceNotAssociatedException; import android.companion.IOnMessageReceivedListener; import android.companion.ISystemDataTransferCallback; import android.companion.datatransfer.PermissionSyncRequest; @@ -56,7 +55,6 @@ import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.association.AssociationStore; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.companion.utils.PackageUtils; -import com.android.server.companion.utils.PermissionsUtils; import java.util.List; import java.util.concurrent.ExecutorService; @@ -120,28 +118,10 @@ public class SystemDataTransferProcessor { } /** - * Resolve the requested association, throwing if the caller doesn't have - * adequate permissions. - */ - @NonNull - private AssociationInfo resolveAssociation(String packageName, int userId, - int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); - } - return association; - } - - /** * Return whether the user has consented to the permission transfer for the association. */ - public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId, - int associationId) { - resolveAssociation(packageName, userId, associationId); + public boolean isPermissionTransferUserConsented(int associationId) { + mAssociationStore.getAssociationWithCallerChecks(associationId); PermissionSyncRequest request = getPermissionSyncRequest(associationId); if (request == null) { @@ -167,7 +147,8 @@ public class SystemDataTransferProcessor { return null; } - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId + "] associationId [" + associationId + "]"); @@ -207,7 +188,7 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + mAssociationStore.getAssociationWithCallerChecks(associationId); // Check if the request has been consented by the user. PermissionSyncRequest request = getPermissionSyncRequest(associationId); @@ -239,24 +220,20 @@ public class SystemDataTransferProcessor { * Enable perm sync for the association */ public void enablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(true); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(true); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** * Disable perm sync for the association */ public void disablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(false); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(false); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** @@ -264,18 +241,17 @@ public class SystemDataTransferProcessor { */ @Nullable public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - return Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - List<SystemDataTransferRequest> requests = - mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, - associationId); - for (SystemDataTransferRequest request : requests) { - if (request instanceof PermissionSyncRequest) { - return (PermissionSyncRequest) request; - } + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId) + .getUserId(); + List<SystemDataTransferRequest> requests = + mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, + associationId); + for (SystemDataTransferRequest request : requests) { + if (request instanceof PermissionSyncRequest) { + return (PermissionSyncRequest) request; } - return null; - }); + } + return null; } /** diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index c89ce11c169d..9c37881499bd 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -33,7 +33,7 @@ import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH; import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST; import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import static java.util.Objects.requireNonNull; diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index cb363a7c9d7f..2d345c48a8eb 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -19,7 +19,7 @@ package com.android.server.companion.presence; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import android.annotation.NonNull; diff --git a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java new file mode 100644 index 000000000000..b6348ea9594d --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.presence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceService; +import android.companion.DevicePresenceEvent; +import android.content.ComponentName; +import android.content.Context; +import android.os.Handler; +import android.util.Pair; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.PerUser; +import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.utils.PackageUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Manages communication with companion applications via + * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to + * the services, maintaining the connection (the binding), and invoking callback methods such as + * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, + * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the + * application process. + * + * <p> + * The following is the list of the APIs provided by {@link CompanionAppBinder} (to be + * utilized by {@link CompanionDeviceManagerService}): + * <ul> + * <li> {@link #bindCompanionApp(int, String, boolean, CompanionServiceConnector.Listener)} + * <li> {@link #unbindCompanionApp(int, String)} + * <li> {@link #isCompanionApplicationBound(int, String)} + * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} + * </ul> + * + * @see CompanionDeviceService + * @see android.companion.ICompanionDeviceService + * @see CompanionServiceConnector + */ +@SuppressLint("LongLogTag") +public class CompanionAppBinder { + private static final String TAG = "CDM_CompanionAppBinder"; + + private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec + + @NonNull + private final Context mContext; + @NonNull + private final CompanionServicesRegister mCompanionServicesRegister; + + @NonNull + @GuardedBy("mBoundCompanionApplications") + private final Map<Pair<Integer, String>, List<CompanionServiceConnector>> + mBoundCompanionApplications; + @NonNull + @GuardedBy("mScheduledForRebindingCompanionApplications") + private final Set<Pair<Integer, String>> mScheduledForRebindingCompanionApplications; + + public CompanionAppBinder(@NonNull Context context) { + mContext = context; + mCompanionServicesRegister = new CompanionServicesRegister(); + mBoundCompanionApplications = new HashMap<>(); + mScheduledForRebindingCompanionApplications = new HashSet<>(); + } + + /** + * On package changed. + */ + public void onPackagesChanged(@UserIdInt int userId) { + mCompanionServicesRegister.invalidate(userId); + } + + /** + * CDM binds to the companion app. + */ + public void bindCompanionApp(@UserIdInt int userId, @NonNull String packageName, + boolean isSelfManaged, CompanionServiceConnector.Listener listener) { + Slog.i(TAG, "Binding user=[" + userId + "], package=[" + packageName + "], isSelfManaged=[" + + isSelfManaged + "]..."); + + final List<ComponentName> companionServices = + mCompanionServicesRegister.forPackage(userId, packageName); + if (companionServices.isEmpty()) { + Slog.e(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " + + "eligible CompanionDeviceService not found.\n" + + "A CompanionDeviceService should declare an intent-filter for " + + "\"android.companion.CompanionDeviceService\" action and require " + + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); + return; + } + + final List<CompanionServiceConnector> serviceConnectors = new ArrayList<>(); + synchronized (mBoundCompanionApplications) { + if (mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) { + Slog.w(TAG, "The package is ALREADY bound."); + return; + } + + for (int i = 0; i < companionServices.size(); i++) { + boolean isPrimary = i == 0; + serviceConnectors.add(CompanionServiceConnector.newInstance(mContext, userId, + companionServices.get(i), isSelfManaged, isPrimary)); + } + + mBoundCompanionApplications.put(new Pair<>(userId, packageName), serviceConnectors); + } + + // Set listeners for both Primary and Secondary connectors. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.setListener(listener); + } + + // Now "bind" all the connectors: the primary one and the rest of them. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.connect(); + } + } + + /** + * CDM unbinds the companion app. + */ + public void unbindCompanionApp(@UserIdInt int userId, @NonNull String packageName) { + Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]..."); + + final List<CompanionServiceConnector> serviceConnectors; + + synchronized (mBoundCompanionApplications) { + serviceConnectors = mBoundCompanionApplications.remove(new Pair<>(userId, packageName)); + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName)); + } + + if (serviceConnectors == null) { + Slog.e(TAG, "The package is not bound."); + return; + } + + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.postUnbind(); + } + } + + /** + * @return whether the companion application is bound now. + */ + public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { + synchronized (mBoundCompanionApplications) { + return mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName)); + } + } + + /** + * Remove bound apps for package. + */ + public void removePackage(int userId, String packageName) { + synchronized (mBoundCompanionApplications) { + mBoundCompanionApplications.remove(new Pair<>(userId, packageName)); + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName)); + } + } + + /** + * Schedule rebinding for the package. + */ + public void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, + CompanionServiceConnector serviceConnector) { + Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); + + if (isRebindingCompanionApplicationScheduled(userId, packageName)) { + Slog.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " + + serviceConnector.getComponentName()); + return; + } + + if (serviceConnector.isPrimary()) { + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.add(new Pair<>(userId, packageName)); + } + } + + // Rebinding in 10 seconds. + Handler.getMain().postDelayed(() -> + onRebindingCompanionApplicationTimeout(userId, packageName, + serviceConnector), + REBIND_TIMEOUT); + } + + private boolean isRebindingCompanionApplicationScheduled( + @UserIdInt int userId, @NonNull String packageName) { + synchronized (mScheduledForRebindingCompanionApplications) { + return mScheduledForRebindingCompanionApplications.contains( + new Pair<>(userId, packageName)); + } + } + + private void onRebindingCompanionApplicationTimeout( + @UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + // Re-mark the application is bound. + if (serviceConnector.isPrimary()) { + synchronized (mBoundCompanionApplications) { + if (!mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) { + List<CompanionServiceConnector> serviceConnectors = + Collections.singletonList(serviceConnector); + mBoundCompanionApplications.put(new Pair<>(userId, packageName), + serviceConnectors); + } + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName)); + } + } + + serviceConnector.connect(); + } + + /** + * Dump bound apps. + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Application Controller: \n"); + + synchronized (mBoundCompanionApplications) { + out.append(" Bound Companion Applications: "); + if (mBoundCompanionApplications.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (Map.Entry<Pair<Integer, String>, List<CompanionServiceConnector>> entry : + mBoundCompanionApplications.entrySet()) { + out.append("<u").append(String.valueOf(entry.getKey().first)).append(", ") + .append(entry.getKey().second).append(">"); + for (CompanionServiceConnector serviceConnector : entry.getValue()) { + out.append(", isPrimary=").append( + String.valueOf(serviceConnector.isPrimary())); + } + } + } + } + + out.append(" Companion Applications Scheduled For Rebinding: "); + synchronized (mScheduledForRebindingCompanionApplications) { + if (mScheduledForRebindingCompanionApplications.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (Pair<Integer, String> app : mScheduledForRebindingCompanionApplications) { + out.append("<u").append(String.valueOf(app.first)).append(", ") + .append(app.second).append(">"); + } + } + } + } + + @Nullable + CompanionServiceConnector getPrimaryServiceConnector( + @UserIdInt int userId, @NonNull String packageName) { + final List<CompanionServiceConnector> connectors; + synchronized (mBoundCompanionApplications) { + connectors = mBoundCompanionApplications.get(new Pair<>(userId, packageName)); + } + return connectors != null ? connectors.get(0) : null; + } + + private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { + @Override + public synchronized @NonNull Map<String, List<ComponentName>> forUser( + @UserIdInt int userId) { + return super.forUser(userId); + } + + synchronized @NonNull List<ComponentName> forPackage( + @UserIdInt int userId, @NonNull String packageName) { + return forUser(userId).getOrDefault(packageName, Collections.emptyList()); + } + + synchronized void invalidate(@UserIdInt int userId) { + remove(userId); + } + + @Override + protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { + return PackageUtils.getCompanionServicesForUser(mContext, userId); + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java deleted file mode 100644 index 7a1a83f53315..000000000000 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ /dev/null @@ -1,620 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion.presence; - -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; -import static android.os.Process.ROOT_UID; -import static android.os.Process.SHELL_UID; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.annotation.TestApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.companion.AssociationInfo; -import android.content.Context; -import android.os.Binder; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelUuid; -import android.os.UserManager; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.companion.association.AssociationStore; - -import java.io.PrintWriter; -import java.util.HashSet; -import java.util.Set; - -/** - * Class responsible for monitoring companion devices' "presence" status (i.e. - * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). - * - * <p> - * Should only be used by - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * to which it provides the following API: - * <ul> - * <li> {@link #onSelfManagedDeviceConnected(int)} - * <li> {@link #onSelfManagedDeviceDisconnected(int)} - * <li> {@link #isDevicePresent(int)} - * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)} - * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)} - * <li> {@link Callback#onDevicePresenceEvent(int, int)}} - * </ul> - */ -@SuppressLint("LongLogTag") -public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener, - BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionDevicePresenceMonitor"; - - /** Callback for notifying about changes to status of companion devices. */ - public interface Callback { - /** Invoked when companion device is found nearby or connects. */ - void onDeviceAppeared(int associationId); - - /** Invoked when a companion device no longer seen nearby or disconnects. */ - void onDeviceDisappeared(int associationId); - - /** Invoked when device has corresponding event changes. */ - void onDevicePresenceEvent(int associationId, int event); - - /** Invoked when device has corresponding event changes base on the UUID */ - void onDevicePresenceEventByUuid(ObservableUuid uuid, int event); - } - - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull Callback mCallback; - private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener; - private final @NonNull BleCompanionDeviceScanner mBleScanner; - - // NOTE: Same association may appear in more than one of the following sets at the same time. - // (E.g. self-managed devices that have MAC addresses, could be reported as present by their - // companion applications, while at the same be connected via BT, or detected nearby by BLE - // scanner) - private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>(); - private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>(); - private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); - private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull Set<Integer> mBtDisconnectedDevices = new HashSet<>(); - - // A map to track device presence within 10 seconds of Bluetooth disconnection. - // The key is the association ID, and the boolean value indicates if the device - // was detected again within that time frame. - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = - new SparseBooleanArray(); - - // Tracking "simulated" presence. Used for debugging and testing only. - private final @NonNull Set<Integer> mSimulated = new HashSet<>(); - private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = - new SimulatedDevicePresenceSchedulerHelper(); - - private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = - new BleDeviceDisappearedScheduler(); - - public CompanionDevicePresenceMonitor(UserManager userManager, - @NonNull AssociationStore associationStore, - @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) { - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mCallback = callback; - mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, - associationStore, mObservableUuidStore, - /* BluetoothCompanionDeviceConnectionListener.Callback */ this); - mBleScanner = new BleCompanionDeviceScanner(associationStore, - /* BleCompanionDeviceScanner.Callback */ this); - } - - /** Initialize {@link CompanionDevicePresenceMonitor} */ - public void init(Context context) { - if (DEBUG) Log.i(TAG, "init()"); - - final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); - if (btAdapter != null) { - mBtConnectionListener.init(btAdapter); - mBleScanner.init(context, btAdapter); - } else { - Log.w(TAG, "BluetoothAdapter is NOT available."); - } - - mAssociationStore.registerLocalListener(this); - } - - /** - * @return current connected UUID devices. - */ - public Set<ParcelUuid> getCurrentConnectedUuidDevices() { - return mConnectedUuidDevices; - } - - /** - * Remove current connected UUID device. - */ - public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { - mConnectedUuidDevices.remove(uuid); - } - - /** - * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); - * or devices is connected (for Bluetooth); or reported (by the application) to be - * nearby (for "self-managed" associations). - */ - public boolean isDevicePresent(int associationId) { - return mReportedSelfManagedDevices.contains(associationId) - || mConnectedBtDevices.contains(associationId) - || mNearbyBleDevices.contains(associationId) - || mSimulated.contains(associationId); - } - - /** - * @return whether the current uuid to be observed is present. - */ - public boolean isDeviceUuidPresent(ParcelUuid uuid) { - return mConnectedUuidDevices.contains(uuid); - } - - /** - * @return whether the current device is BT connected and had already reported to the app. - */ - - public boolean isBtConnected(int associationId) { - return mConnectedBtDevices.contains(associationId); - } - - /** - * @return whether the current device in BLE range and had already reported to the app. - */ - public boolean isBlePresent(int associationId) { - return mNearbyBleDevices.contains(associationId); - } - - /** - * @return whether the current device had been already reported by the simulator. - */ - public boolean isSimulatePresent(int associationId) { - return mSimulated.contains(associationId); - } - - /** - * Marks a "self-managed" device as connected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()} - */ - public void onSelfManagedDeviceConnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_APPEARED); - } - - /** - * Marks a "self-managed" device as disconnected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()} - */ - public void onSelfManagedDeviceDisconnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - /** - * Marks a "self-managed" device as disconnected when binderDied. - */ - public void onSelfManagedDeviceReporterBinderDied(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - @Override - public void onBluetoothCompanionDeviceConnected(int associationId) { - synchronized (mBtDisconnectedDevices) { - // A device is considered reconnected within 10 seconds if a pending BLE lost report is - // followed by a detected Bluetooth connection. - boolean isReconnected = mBtDisconnectedDevices.contains(associationId); - if (isReconnected) { - Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - - Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " - + "associationId( " + associationId + " )"); - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); - - // Stop the BLE scan if all devices report BT connected status and BLE was present. - if (canStopBleScan()) { - mBleScanner.stopScanIfNeeded(); - } - - } - } - - @Override - public void onBluetoothCompanionDeviceDisconnected(int associationId) { - Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " - + "associationId( " + associationId + " )"); - // Start BLE scanning when the device is disconnected. - mBleScanner.startScan(); - - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); - // If current device is BLE present but BT is disconnected , means it will be - // potentially out of range later. Schedule BLE disappeared callback. - if (isBlePresent(associationId)) { - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.add(associationId); - } - mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); - } - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - final ParcelUuid parcelUuid = uuid.getUuid(); - - switch(event) { - case EVENT_BT_CONNECTED: - boolean added = mConnectedUuidDevices.add(parcelUuid); - - if (!added) { - Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as " - + "present by this event=" + event); - } - - break; - case EVENT_BT_DISCONNECTED: - final boolean removed = mConnectedUuidDevices.remove(parcelUuid); - - if (!removed) { - Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported " - + "as present by this event= " + event); - - return; - } - - break; - } - - mCallback.onDevicePresenceEventByUuid(uuid, event); - } - - - @Override - public void onBleCompanionDeviceFound(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); - if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - } - } - - @Override - public void onBleCompanionDeviceLost(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEvent(int associationId, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - // Make sure the association exists. - enforceAssociationExists(associationId); - - switch (event) { - case EVENT_BLE_APPEARED: - simulateDeviceAppeared(associationId, event); - break; - case EVENT_BT_CONNECTED: - onBluetoothCompanionDeviceConnected(associationId); - break; - case EVENT_BLE_DISAPPEARED: - simulateDeviceDisappeared(associationId, event); - break; - case EVENT_BT_DISCONNECTED: - onBluetoothCompanionDeviceDisconnected(associationId); - break; - default: - throw new IllegalArgumentException("Event: " + event + "is not supported"); - } - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - onDevicePresenceEventByUuid(uuid, event); - } - - private void simulateDeviceAppeared(int associationId, int state) { - onDevicePresenceEvent(mSimulated, associationId, state); - mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - } - - private void simulateDeviceDisappeared(int associationId, int state) { - mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - onDevicePresenceEvent(mSimulated, associationId, state); - } - - private void enforceAssociationExists(int associationId) { - if (mAssociationStore.getAssociationById(associationId) == null) { - throw new IllegalArgumentException( - "Association with id " + associationId + " does not exist."); - } - } - - private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, - int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event); - - switch (event) { - case EVENT_BLE_APPEARED: - synchronized (mBtDisconnectedDevices) { - // If a BLE device is detected within 10 seconds after BT is disconnected, - // flag it as BLE is present. - if (mBtDisconnectedDevices.contains(associationId)) { - Slog.i(TAG, "Device ( " + associationId + " ) is present," - + " do not need to send the callback with event ( " - + EVENT_BLE_APPEARED + " )."); - mBtDisconnectedDevicesBlePresence.append(associationId, true); - } - } - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - final boolean added = presentDevicesForSource.add(associationId); - - if (!added) { - Slog.w(TAG, "Association with id " - + associationId + " is ALREADY reported as " - + "present by this source, event=" + event); - } - - mCallback.onDeviceAppeared(associationId); - - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - final boolean removed = presentDevicesForSource.remove(associationId); - - if (!removed) { - Slog.w(TAG, "Association with id " + associationId + " was NOT reported " - + "as present by this source, event= " + event); - - return; - } - - mCallback.onDeviceDisappeared(associationId); - - break; - default: - Slog.e(TAG, "Event: " + event + " is not supported"); - return; - } - - mCallback.onDevicePresenceEvent(associationId, event); - } - - /** - * Implements - * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} - */ - @Override - public void onAssociationRemoved(@NonNull AssociationInfo association) { - final int id = association.getId(); - if (DEBUG) { - Log.i(TAG, "onAssociationRemoved() id=" + id); - Log.d(TAG, " > association=" + association); - } - - mConnectedBtDevices.remove(id); - mNearbyBleDevices.remove(id); - mReportedSelfManagedDevices.remove(id); - mSimulated.remove(id); - mBtDisconnectedDevices.remove(id); - mBtDisconnectedDevicesBlePresence.delete(id); - - // Do NOT call mCallback.onDeviceDisappeared()! - // CompanionDeviceManagerService will know that the association is removed, and will do - // what's needed. - } - - /** - * Return a set of devices that pending to report connectivity - */ - public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { - synchronized (mBtConnectionListener.mPendingConnectedDevices) { - return mBtConnectionListener.mPendingConnectedDevices; - } - } - - private static void enforceCallerShellOrRoot() { - final int callingUid = Binder.getCallingUid(); - if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; - - throw new SecurityException("Caller is neither Shell nor Root"); - } - - /** - * The BLE scan can be only stopped if all the devices have been reported - * BT connected and BLE presence and are not pending to report BLE lost. - */ - private boolean canStopBleScan() { - for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { - int id = ai.getId(); - synchronized (mBtDisconnectedDevices) { - if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) - && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { - Slog.i(TAG, "The BLE scan cannot be stopped, " - + "device( " + id + " ) is not yet connected " - + "OR the BLE is not current present Or is pending to report BLE lost"); - return false; - } - } - } - return true; - } - - /** - * Dumps system information about devices that are marked as "present". - */ - public void dump(@NonNull PrintWriter out) { - out.append("Companion Device Present: "); - if (mConnectedBtDevices.isEmpty() - && mNearbyBleDevices.isEmpty() - && mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - return; - } else { - out.append("\n"); - } - - out.append(" Connected Bluetooth Devices: "); - if (mConnectedBtDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mConnectedBtDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Nearby BLE Devices: "); - if (mNearbyBleDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mNearbyBleDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Self-Reported Devices: "); - if (mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mReportedSelfManagedDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - } - - private class SimulatedDevicePresenceSchedulerHelper extends Handler { - SimulatedDevicePresenceSchedulerHelper() { - super(Looper.getMainLooper()); - } - - void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - // First, unschedule if it was scheduled previously. - if (hasMessages(/* what */ associationId)) { - removeMessages(/* what */ associationId); - } - - sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); - } - - void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - removeMessages(/* what */ associationId); - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - if (mSimulated.contains(associationId)) { - onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); - } - } - } - - private class BleDeviceDisappearedScheduler extends Handler { - BleDeviceDisappearedScheduler() { - super(Looper.getMainLooper()); - } - - void scheduleBleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - removeMessages(associationId); - } - Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); - sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); - } - - void unScheduleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - - removeMessages(associationId); - } - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( - associationId); - // If a device hasn't reported after 10 seconds and is not currently present, - // assume BLE is lost and trigger the onDeviceEvent callback with the - // EVENT_BLE_DISAPPEARED event. - if (mBtDisconnectedDevices.contains(associationId) - && !isCurrentPresent) { - Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " - + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java index 5abdb42b34fc..c01c3195e04d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.presence; import static android.content.Context.BIND_ALMOST_PERCEPTIBLE; import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE; @@ -33,36 +33,42 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; -import android.util.Log; +import android.util.Slog; import com.android.internal.infra.ServiceConnector; import com.android.server.ServiceThread; +import com.android.server.companion.CompanionDeviceManagerService; /** * Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the * application process. */ @SuppressLint("LongLogTag") -class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - private static final String TAG = "CDM_CompanionServiceConnector"; - private static final boolean DEBUG = false; +public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ - private static final long UNBIND_POST_DELAY_MS = 5_000; - - /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector} */ - interface Listener { + /** Listener for changes to the state of the {@link CompanionServiceConnector} */ + public interface Listener { + /** + * Called when service binding is died. + */ void onBindingDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector); + @NonNull CompanionServiceConnector serviceConnector); } - private final @UserIdInt int mUserId; - private final @NonNull ComponentName mComponentName; + private static final String TAG = "CDM_CompanionServiceConnector"; + + /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ + private static final long UNBIND_POST_DELAY_MS = 5_000; + @UserIdInt + private final int mUserId; + @NonNull + private final ComponentName mComponentName; + private final boolean mIsPrimary; // IMPORTANT: this can (and will!) be null (at the moment, CompanionApplicationController only // installs a listener to the primary ServiceConnector), hence we should always null-check the // reference before calling on it. - private @Nullable Listener mListener; - private boolean mIsPrimary; + @Nullable + private Listener mListener; /** * Create a CompanionDeviceServiceConnector instance. @@ -79,16 +85,16 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the * service importance level should be higher than 125. */ - static CompanionDeviceServiceConnector newInstance(@NonNull Context context, + static CompanionServiceConnector newInstance(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, boolean isSelfManaged, boolean isPrimary) { final int bindingFlags = isSelfManaged ? BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE : BIND_ALMOST_PERCEPTIBLE; - return new CompanionDeviceServiceConnector( + return new CompanionServiceConnector( context, userId, componentName, bindingFlags, isPrimary); } - private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId, + private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) { super(context, buildIntent(componentName), bindingFlags, userId, null); mUserId = userId; @@ -133,6 +139,7 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return mIsPrimary; } + @NonNull ComponentName getComponentName() { return mComponentName; } @@ -140,17 +147,15 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe @Override protected void onServiceConnectionStatusChanged( @NonNull ICompanionDeviceService service, boolean isConnected) { - if (DEBUG) { - Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString() - + " connected=" + isConnected); - } + Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString() + + " connected=" + isConnected); } @Override public void binderDied() { super.binderDied(); - if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString()); + Slog.d(TAG, "binderDied() " + mComponentName.toShortString()); // Handle primary process being killed if (mListener != null) { @@ -172,7 +177,8 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * within system_server and thus tends to get heavily congested) */ @Override - protected @NonNull Handler getJobHandler() { + @NonNull + protected Handler getJobHandler() { return getServiceThread().getThreadHandler(); } @@ -182,12 +188,14 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return -1; } - private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) { + @NonNull + private static Intent buildIntent(@NonNull ComponentName componentName) { return new Intent(CompanionDeviceService.SERVICE_INTERFACE) .setComponent(componentName); } - private static @NonNull ServiceThread getServiceThread() { + @NonNull + private static ServiceThread getServiceThread() { if (sServiceThread == null) { synchronized (CompanionDeviceManagerService.class) { if (sServiceThread == null) { @@ -206,5 +214,6 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * <p> * Do NOT reference directly, use {@link #getServiceThread()} method instead. */ - private static volatile @Nullable ServiceThread sServiceThread; + @Nullable + private static volatile ServiceThread sServiceThread; } diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java new file mode 100644 index 000000000000..642460e64216 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java @@ -0,0 +1,1042 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion.presence; + +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; +import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; +import static android.companion.DevicePresenceEvent.NO_ASSOCIATION; +import static android.os.Process.ROOT_UID; +import static android.os.Process.SHELL_UID; + +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObserveDevicePresenceByUuid; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.TestApi; +import android.annotation.UserIdInt; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.companion.AssociationInfo; +import android.companion.DeviceNotAssociatedException; +import android.companion.DevicePresenceEvent; +import android.companion.ObservingDevicePresenceRequest; +import android.content.Context; +import android.hardware.power.Mode; +import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.PowerManagerInternal; +import android.os.RemoteException; +import android.os.UserManager; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.companion.association.AssociationStore; + +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class responsible for monitoring companion devices' "presence" status (i.e. + * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). + * + * <p> + * Should only be used by + * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} + * to which it provides the following API: + * <ul> + * <li> {@link #onSelfManagedDeviceConnected(int)} + * <li> {@link #onSelfManagedDeviceDisconnected(int)} + * <li> {@link #isDevicePresent(int)} + * </ul> + */ +@SuppressLint("LongLogTag") +public class DevicePresenceProcessor implements AssociationStore.OnChangeListener, + BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { + static final boolean DEBUG = false; + private static final String TAG = "CDM_DevicePresenceProcessor"; + + @NonNull + private final Context mContext; + @NonNull + private final CompanionAppBinder mCompanionAppBinder; + @NonNull + private final AssociationStore mAssociationStore; + @NonNull + private final ObservableUuidStore mObservableUuidStore; + @NonNull + private final BluetoothCompanionDeviceConnectionListener mBtConnectionListener; + @NonNull + private final BleCompanionDeviceScanner mBleScanner; + @NonNull + private final PowerManagerInternal mPowerManagerInternal; + + // NOTE: Same association may appear in more than one of the following sets at the same time. + // (E.g. self-managed devices that have MAC addresses, could be reported as present by their + // companion applications, while at the same be connected via BT, or detected nearby by BLE + // scanner) + @NonNull + private final Set<Integer> mConnectedBtDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mNearbyBleDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); + @NonNull + private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); + @NonNull + @GuardedBy("mBtDisconnectedDevices") + private final Set<Integer> mBtDisconnectedDevices = new HashSet<>(); + + // A map to track device presence within 10 seconds of Bluetooth disconnection. + // The key is the association ID, and the boolean value indicates if the device + // was detected again within that time frame. + @GuardedBy("mBtDisconnectedDevices") + private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = + new SparseBooleanArray(); + + // Tracking "simulated" presence. Used for debugging and testing only. + private final @NonNull Set<Integer> mSimulated = new HashSet<>(); + private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = + new SimulatedDevicePresenceSchedulerHelper(); + + private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = + new BleDeviceDisappearedScheduler(); + + public DevicePresenceProcessor(@NonNull Context context, + @NonNull CompanionAppBinder companionAppBinder, + UserManager userManager, + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, + @NonNull PowerManagerInternal powerManagerInternal) { + mContext = context; + mCompanionAppBinder = companionAppBinder; + mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; + mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, + associationStore, mObservableUuidStore, + /* BluetoothCompanionDeviceConnectionListener.Callback */ this); + mBleScanner = new BleCompanionDeviceScanner(associationStore, + /* BleCompanionDeviceScanner.Callback */ this); + mPowerManagerInternal = powerManagerInternal; + } + + /** Initialize {@link DevicePresenceProcessor} */ + public void init(Context context) { + if (DEBUG) Slog.i(TAG, "init()"); + + final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); + if (btAdapter != null) { + mBtConnectionListener.init(btAdapter); + mBleScanner.init(context, btAdapter); + } else { + Slog.w(TAG, "BluetoothAdapter is NOT available."); + } + + mAssociationStore.registerLocalListener(this); + } + + /** + * Process device presence start request. + */ + public void startObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Start observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + // If it's already being observed, then no-op. + if (mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already being observed."); + return; + } + + final ObservableUuid observableUuid = new ObservableUuid(userId, requestUuid, + packageName, System.currentTimeMillis()); + mObservableUuidStore.writeObservableUuid(userId, observableUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(true) + .build(); + mAssociationStore.updateAssociation(association); + + // Send callback immediately if the device is present. + if (isDevicePresent(associationId)) { + Slog.i(TAG, "Device is already present. Triggering callback."); + if (isBlePresent(associationId)) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + } else if (isBtConnected(associationId)) { + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + } else if (isSimulatePresent(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_APPEARED); + } + } + } + + Slog.i(TAG, "Registered device presence listener."); + } + + /** + * Process device presence stop request. + */ + public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Stop observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + if (!mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already not being observed."); + return; + } + + mObservableUuidStore.removeObservableUuid(userId, requestUuid, packageName); + removeCurrentConnectedUuidDevice(requestUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (!association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already not being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(false) + .build(); + mAssociationStore.updateAssociation(association); + } + + Slog.i(TAG, "Unregistered device presence listener."); + + // If last listener is unregistered, then unbind application. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApp(userId, packageName); + } + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #startObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void startObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Start observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + startObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #stopObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void stopObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Stop observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + stopObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * @return whether the package should be bound (i.e. at least one of the devices associated with + * the package is currently present OR the UUID to be observed by this package is + * currently present). + */ + private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { + final List<AssociationInfo> packageAssociations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo association : packageAssociations) { + if (!association.shouldBindWhenPresent()) continue; + if (isDevicePresent(association.getId())) return true; + } + + for (ObservableUuid uuid : observableUuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + return true; + } + } + + return false; + } + + /** + * Bind the system to the app if it's not bound. + * + * Set bindImportant to true when the association is self-managed to avoid the target service + * being killed. + */ + private void bindApplicationIfNeeded(int userId, String packageName, boolean bindImportant) { + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + mCompanionAppBinder.bindCompanionApp( + userId, packageName, bindImportant, this::onBinderDied); + } else { + Slog.i(TAG, + "UserId=[" + userId + "], packageName=[" + packageName + "] is already bound."); + } + } + + /** + * @return current connected UUID devices. + */ + public Set<ParcelUuid> getCurrentConnectedUuidDevices() { + return mConnectedUuidDevices; + } + + /** + * Remove current connected UUID device. + */ + public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { + mConnectedUuidDevices.remove(uuid); + } + + /** + * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); + * or devices is connected (for Bluetooth); or reported (by the application) to be + * nearby (for "self-managed" associations). + */ + public boolean isDevicePresent(int associationId) { + return mReportedSelfManagedDevices.contains(associationId) + || mConnectedBtDevices.contains(associationId) + || mNearbyBleDevices.contains(associationId) + || mSimulated.contains(associationId); + } + + /** + * @return whether the current uuid to be observed is present. + */ + public boolean isDeviceUuidPresent(ParcelUuid uuid) { + return mConnectedUuidDevices.contains(uuid); + } + + /** + * @return whether the current device is BT connected and had already reported to the app. + */ + + public boolean isBtConnected(int associationId) { + return mConnectedBtDevices.contains(associationId); + } + + /** + * @return whether the current device in BLE range and had already reported to the app. + */ + public boolean isBlePresent(int associationId) { + return mNearbyBleDevices.contains(associationId); + } + + /** + * @return whether the current device had been already reported by the simulator. + */ + public boolean isSimulatePresent(int associationId) { + return mSimulated.contains(associationId); + } + + /** + * Marks a "self-managed" device as connected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) + * notifyDeviceAppeared()} + */ + public void onSelfManagedDeviceConnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_APPEARED); + } + + /** + * Marks a "self-managed" device as disconnected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) + * notifyDeviceDisappeared()} + */ + public void onSelfManagedDeviceDisconnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + /** + * Marks a "self-managed" device as disconnected when binderDied. + */ + public void onSelfManagedDeviceReporterBinderDied(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + @Override + public void onBluetoothCompanionDeviceConnected(int associationId) { + synchronized (mBtDisconnectedDevices) { + // A device is considered reconnected within 10 seconds if a pending BLE lost report is + // followed by a detected Bluetooth connection. + boolean isReconnected = mBtDisconnectedDevices.contains(associationId); + if (isReconnected) { + Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + + Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " + + "associationId( " + associationId + " )"); + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + + // Stop the BLE scan if all devices report BT connected status and BLE was present. + if (canStopBleScan()) { + mBleScanner.stopScanIfNeeded(); + } + + } + } + + @Override + public void onBluetoothCompanionDeviceDisconnected(int associationId) { + Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " + + "associationId( " + associationId + " )"); + // Start BLE scanning when the device is disconnected. + mBleScanner.startScan(); + + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); + // If current device is BLE present but BT is disconnected , means it will be + // potentially out of range later. Schedule BLE disappeared callback. + if (isBlePresent(associationId)) { + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.add(associationId); + } + mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); + } + } + + + @Override + public void onBleCompanionDeviceFound(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); + if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + } + } + + @Override + public void onBleCompanionDeviceLost(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEvent(int associationId, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + // Make sure the association exists. + enforceAssociationExists(associationId); + + switch (event) { + case EVENT_BLE_APPEARED: + simulateDeviceAppeared(associationId, event); + break; + case EVENT_BT_CONNECTED: + onBluetoothCompanionDeviceConnected(associationId); + break; + case EVENT_BLE_DISAPPEARED: + simulateDeviceDisappeared(associationId, event); + break; + case EVENT_BT_DISCONNECTED: + onBluetoothCompanionDeviceDisconnected(associationId); + break; + default: + throw new IllegalArgumentException("Event: " + event + "is not supported"); + } + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + onDevicePresenceEventByUuid(uuid, event); + } + + private void simulateDeviceAppeared(int associationId, int state) { + onDevicePresenceEvent(mSimulated, associationId, state); + mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + } + + private void simulateDeviceDisappeared(int associationId, int state) { + mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + onDevicePresenceEvent(mSimulated, associationId, state); + } + + private void enforceAssociationExists(int associationId) { + if (mAssociationStore.getAssociationById(associationId) == null) { + throw new IllegalArgumentException( + "Association with id " + associationId + " does not exist."); + } + } + + private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, + int associationId, int eventType) { + Slog.i(TAG, + "onDevicePresenceEvent() id=[" + associationId + "], event=[" + eventType + "]..."); + + AssociationInfo association = mAssociationStore.getAssociationById(associationId); + if (association == null) { + Slog.e(TAG, "Association doesn't exist."); + return; + } + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final DevicePresenceEvent event = new DevicePresenceEvent(associationId, eventType, null); + + if (eventType == EVENT_BLE_APPEARED) { + synchronized (mBtDisconnectedDevices) { + // If a BLE device is detected within 10 seconds after BT is disconnected, + // flag it as BLE is present. + if (mBtDisconnectedDevices.contains(associationId)) { + Slog.i(TAG, "Device ( " + associationId + " ) is present," + + " do not need to send the callback with event ( " + + EVENT_BLE_APPEARED + " )."); + mBtDisconnectedDevicesBlePresence.append(associationId, true); + } + } + } + + switch (eventType) { + case EVENT_BLE_APPEARED: + case EVENT_BT_CONNECTED: + case EVENT_SELF_MANAGED_APPEARED: + final boolean added = presentDevicesForSource.add(associationId); + if (!added) { + Slog.w(TAG, "The association is already present."); + } + + if (association.shouldBindWhenPresent()) { + bindApplicationIfNeeded(userId, packageName, association.isSelfManaged()); + } else { + return; + } + + if (association.isSelfManaged() || added) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, true); + } + break; + case EVENT_BLE_DISAPPEARED: + case EVENT_BT_DISCONNECTED: + case EVENT_SELF_MANAGED_DISAPPEARED: + final boolean removed = presentDevicesForSource.remove(associationId); + if (!removed) { + Slog.w(TAG, "The association is already NOT present."); + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound"); + return; + } + + if (association.isSelfManaged() || removed) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, false); + } + + // Check if there are other devices associated to the app that are present. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApp(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported."); + break; + } + } + + @Override + public void onDevicePresenceEventByUuid(ObservableUuid uuid, int eventType) { + Slog.i(TAG, "onDevicePresenceEventByUuid ObservableUuid=[" + uuid + "], event=[" + eventType + + "]..."); + + final ParcelUuid parcelUuid = uuid.getUuid(); + final String packageName = uuid.getPackageName(); + final int userId = uuid.getUserId(); + final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType, + parcelUuid); + + switch (eventType) { + case EVENT_BT_CONNECTED: + boolean added = mConnectedUuidDevices.add(parcelUuid); + if (!added) { + Slog.w(TAG, "This device is already connected."); + } + + bindApplicationIfNeeded(userId, packageName, false); + + notifyDevicePresenceEvent(userId, packageName, event); + break; + case EVENT_BT_DISCONNECTED: + final boolean removed = mConnectedUuidDevices.remove(parcelUuid); + if (!removed) { + Slog.w(TAG, "This device is already disconnected."); + return; + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound."); + return; + } + + notifyDevicePresenceEvent(userId, packageName, event); + + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApp(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported"); + break; + } + } + + /** + * Notify device presence event to the app. + * + * @deprecated Use {@link #notifyDevicePresenceEvent(int, String, DevicePresenceEvent)} instead. + */ + @Deprecated + private void legacyNotifyDevicePresenceEvent(AssociationInfo association, + boolean isAppeared) { + Slog.i(TAG, "legacyNotifyDevicePresenceEvent() association=[" + association.toShortString() + + "], isAppeared=[" + isAppeared + "]"); + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is not bound."); + return; + } + + if (isAppeared) { + primaryServiceConnector.postOnDeviceAppeared(association); + } else { + primaryServiceConnector.postOnDeviceDisappeared(association); + } + } + + /** + * Notify the device presence event to the app. + */ + private void notifyDevicePresenceEvent(int userId, String packageName, + DevicePresenceEvent event) { + Slog.i(TAG, + "notifyCompanionDevicePresenceEvent userId=[" + userId + "], packageName=[" + + packageName + "], event=[" + event + "]..."); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is NOT bound."); + return; + } + + primaryServiceConnector.postOnDevicePresenceEvent(event); + } + + /** + * Notify the self-managed device presence event to the app. + */ + public void notifySelfManagedDevicePresenceEvent(int associationId, boolean isAppeared) { + Slog.i(TAG, "notifySelfManagedDeviceAppeared() id=" + associationId); + + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + if (!association.isSelfManaged()) { + throw new IllegalArgumentException("Association id=[" + associationId + + "] is not self-managed."); + } + // AssociationInfo class is immutable: create a new AssociationInfo object with updated + // timestamp. + association = (new AssociationInfo.Builder(association)) + .setLastTimeConnected(System.currentTimeMillis()) + .build(); + mAssociationStore.updateAssociation(association); + + if (isAppeared) { + onSelfManagedDeviceConnected(associationId); + } else { + onSelfManagedDeviceDisconnected(associationId); + } + + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, isAppeared); + } + } + + private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + + boolean isPrimary = serviceConnector.isPrimary(); + Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); + + // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. + if (isPrimary) { + final List<AssociationInfo> associations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + + for (AssociationInfo association : associations) { + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); + break; + } + } + + mCompanionAppBinder.removePackage(userId, packageName); + } + + // Second: schedule rebinding if needed. + final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); + + if (shouldScheduleRebind) { + mCompanionAppBinder.scheduleRebinding(userId, packageName, serviceConnector); + } + } + + /** + * Check if the system should rebind the self-managed secondary services + * OR non-self-managed services. + */ + private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { + // Make sure do not schedule rebind for the case ServiceConnector still gets callback after + // app is uninstalled. + boolean stillAssociated = false; + // Make sure to clean up the state for all the associations + // that associate with this package. + boolean shouldScheduleRebind = false; + boolean shouldScheduleRebindForUuid = false; + final List<ObservableUuid> uuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo ai : + mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { + final int associationId = ai.getId(); + stillAssociated = true; + if (ai.isSelfManaged()) { + // Do not rebind if primary one is died for selfManaged application. + if (isPrimary && isDevicePresent(associationId)) { + onSelfManagedDeviceReporterBinderDied(associationId); + shouldScheduleRebind = false; + } + // Do not rebind if both primary and secondary services are died for + // selfManaged application. + shouldScheduleRebind = mCompanionAppBinder.isCompanionApplicationBound(userId, + packageName); + } else if (ai.isNotifyOnDeviceNearby()) { + // Always rebind for non-selfManaged devices. + shouldScheduleRebind = true; + } + } + + for (ObservableUuid uuid : uuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + shouldScheduleRebindForUuid = true; + break; + } + } + + return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; + } + + /** + * Implements + * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} + */ + @Override + public void onAssociationRemoved(@NonNull AssociationInfo association) { + final int id = association.getId(); + if (DEBUG) { + Log.i(TAG, "onAssociationRemoved() id=" + id); + Log.d(TAG, " > association=" + association); + } + + mConnectedBtDevices.remove(id); + mNearbyBleDevices.remove(id); + mReportedSelfManagedDevices.remove(id); + mSimulated.remove(id); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(id); + mBtDisconnectedDevicesBlePresence.delete(id); + } + + // Do NOT call mCallback.onDeviceDisappeared()! + // CompanionDeviceManagerService will know that the association is removed, and will do + // what's needed. + } + + /** + * Return a set of devices that pending to report connectivity + */ + public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { + synchronized (mBtConnectionListener.mPendingConnectedDevices) { + return mBtConnectionListener.mPendingConnectedDevices; + } + } + + private static void enforceCallerShellOrRoot() { + final int callingUid = Binder.getCallingUid(); + if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; + + throw new SecurityException("Caller is neither Shell nor Root"); + } + + /** + * The BLE scan can be only stopped if all the devices have been reported + * BT connected and BLE presence and are not pending to report BLE lost. + */ + private boolean canStopBleScan() { + for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { + int id = ai.getId(); + synchronized (mBtDisconnectedDevices) { + if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) + && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { + Slog.i(TAG, "The BLE scan cannot be stopped, " + + "device( " + id + " ) is not yet connected " + + "OR the BLE is not current present Or is pending to report BLE lost"); + return false; + } + } + } + return true; + } + + /** + * Dumps system information about devices that are marked as "present". + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Present: "); + if (mConnectedBtDevices.isEmpty() + && mNearbyBleDevices.isEmpty() + && mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + return; + } else { + out.append("\n"); + } + + out.append(" Connected Bluetooth Devices: "); + if (mConnectedBtDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mConnectedBtDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Nearby BLE Devices: "); + if (mNearbyBleDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mNearbyBleDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Self-Reported Devices: "); + if (mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mReportedSelfManagedDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + } + + private class SimulatedDevicePresenceSchedulerHelper extends Handler { + SimulatedDevicePresenceSchedulerHelper() { + super(Looper.getMainLooper()); + } + + void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + // First, unschedule if it was scheduled previously. + if (hasMessages(/* what */ associationId)) { + removeMessages(/* what */ associationId); + } + + sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); + } + + void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + removeMessages(/* what */ associationId); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + if (mSimulated.contains(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); + } + } + } + + private class BleDeviceDisappearedScheduler extends Handler { + BleDeviceDisappearedScheduler() { + super(Looper.getMainLooper()); + } + + void scheduleBleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + removeMessages(associationId); + } + Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); + sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); + } + + void unScheduleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + + removeMessages(associationId); + } + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( + associationId); + // If a device hasn't reported after 10 seconds and is not currently present, + // assume BLE is lost and trigger the onDeviceEvent callback with the + // EVENT_BLE_DISAPPEARED event. + if (mBtDisconnectedDevices.contains(associationId) + && !isCurrentPresent) { + Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " + + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java index db15da2922cf..fa0f6bd92acb 100644 --- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java +++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java @@ -300,4 +300,18 @@ public class ObservableUuidStore { return readObservableUuidsFromCache(userId); } } + + /** + * Check if a UUID is being observed by the package. + */ + public boolean isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName) { + final List<ObservableUuid> uuidsBeingObserved = getObservableUuidsForPackage(userId, + packageName); + for (ObservableUuid observableUuid : uuidsBeingObserved) { + if (observableUuid.getUuid().equals(uuid)) { + return true; + } + } + return false; + } } diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 793fb7ff74b1..697ef87b5a12 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -46,7 +46,6 @@ import java.util.concurrent.Future; @SuppressLint("LongLogTag") public class CompanionTransportManager { private static final String TAG = "CDM_CompanionTransportManager"; - private static final boolean DEBUG = false; private boolean mSecureTransportEnabled = true; @@ -137,11 +136,17 @@ public class CompanionTransportManager { } } - public void attachSystemDataTransport(String packageName, int userId, int associationId, - ParcelFileDescriptor fd) { + /** + * Attach transport. + */ + public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { + Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { if (mTransports.contains(associationId)) { - detachSystemDataTransport(packageName, userId, associationId); + detachSystemDataTransport(associationId); } // TODO: Implement new API to pass a PSK @@ -149,9 +154,18 @@ public class CompanionTransportManager { notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport attached."); } - public void detachSystemDataTransport(String packageName, int userId, int associationId) { + /** + * Detach transport. + */ + public void detachSystemDataTransport(int associationId) { + Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { final Transport transport = mTransports.removeReturnOld(associationId); if (transport == null) { @@ -161,6 +175,8 @@ public class CompanionTransportManager { transport.stop(); notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport detached."); } private void notifyOnTransportsChanged() { @@ -307,8 +323,7 @@ public class CompanionTransportManager { int associationId = transport.mAssociationId; AssociationInfo association = mAssociationStore.getAssociationById(associationId); if (association != null) { - detachSystemDataTransport(association.getPackageName(), - association.getUserId(), + detachSystemDataTransport( association.getId()); } } diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java index 2cf1f462a7d1..d7e766eed209 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -39,7 +39,6 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.content.Context; @@ -208,7 +207,7 @@ public final class PermissionsUtils { /** * Require the caller to hold necessary permission to observe device presence by UUID. */ - public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) { + public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) { if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) != PERMISSION_GRANTED) { throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " @@ -235,23 +234,6 @@ public final class PermissionsUtils { return checkCallerCanManageCompanionDevice(context); } - /** - * Check if CDM can trust the context to process the association. - */ - @Nullable - public static AssociationInfo sanitizeWithCallerChecks(@NonNull Context context, - @Nullable AssociationInfo association) { - if (association == null) return null; - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - if (!checkCallerCanManageAssociationsForPackage(context, userId, packageName)) { - return null; - } - - return association; - } - private static boolean checkPackage(@UserIdInt int uid, @NonNull String packageName) { try { return getAppOpsService().checkPackage(uid, packageName) == MODE_ALLOWED; diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 272e84b80870..f7ed702f1044 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -163,7 +163,6 @@ final class ActivityManagerConstants extends ContentObserver { static final String KEY_USE_TIERED_CACHED_ADJ = "use_tiered_cached_adj"; static final String KEY_TIERED_CACHED_ADJ_DECAY_TIME = "tiered_cached_adj_decay_time"; - static final String KEY_USE_MODERN_TRIM = "use_modern_trim"; /** * Whether or not to enable the new oom adjuster implementation. @@ -239,8 +238,6 @@ final class ActivityManagerConstants extends ContentObserver { private static final boolean DEFAULT_USE_TIERED_CACHED_ADJ = false; private static final long DEFAULT_TIERED_CACHED_ADJ_DECAY_TIME = 60 * 1000; - private static final boolean DEFAULT_USE_MODERN_TRIM = true; - /** * The default value to {@link #KEY_ENABLE_NEW_OOMADJ}. */ @@ -1136,9 +1133,6 @@ final class ActivityManagerConstants extends ContentObserver { /** @see #KEY_TIERED_CACHED_ADJ_DECAY_TIME */ public long TIERED_CACHED_ADJ_DECAY_TIME = DEFAULT_TIERED_CACHED_ADJ_DECAY_TIME; - /** @see #KEY_USE_MODERN_TRIM */ - public boolean USE_MODERN_TRIM = DEFAULT_USE_MODERN_TRIM; - /** @see #KEY_ENABLE_NEW_OOMADJ */ public boolean ENABLE_NEW_OOMADJ = DEFAULT_ENABLE_NEW_OOM_ADJ; @@ -1343,9 +1337,6 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_TIERED_CACHED_ADJ_DECAY_TIME: updateUseTieredCachedAdj(); break; - case KEY_USE_MODERN_TRIM: - updateUseModernTrim(); - break; case KEY_DISABLE_APP_PROFILER_PSS_PROFILING: updateDisableAppProfilerPssProfiling(); break; @@ -2233,13 +2224,6 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_TIERED_CACHED_ADJ_DECAY_TIME); } - private void updateUseModernTrim() { - USE_MODERN_TRIM = DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, - KEY_USE_MODERN_TRIM, - DEFAULT_USE_MODERN_TRIM); - } - private void updateEnableNewOomAdj() { ENABLE_NEW_OOMADJ = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT, diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 48daef801245..51aae771542e 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -1357,7 +1357,7 @@ public class AppProfiler { } @GuardedBy({"mService", "mProcLock"}) - boolean updateLowMemStateLSP(int numCached, int numEmpty, int numTrimming, long now) { + void updateLowMemStateLSP(int numCached, int numEmpty, int numTrimming, long now) { int memFactor; if (mLowMemDetector != null && mLowMemDetector.isAvailable()) { memFactor = mLowMemDetector.getMemFactor(); @@ -1422,114 +1422,37 @@ public class AppProfiler { mLastMemoryLevel = memFactor; mLastNumProcesses = mService.mProcessList.getLruSizeLOSP(); - if (mService.mConstants.USE_MODERN_TRIM) { - // Modern trim is not sent based on lowmem state - // Dispatch UI_HIDDEN to processes that need it - mService.mProcessList.forEachLruProcessesLOSP(true, app -> { - final ProcessProfileRecord profile = app.mProfile; - final IApplicationThread thread; - final ProcessStateRecord state = app.mState; - if (state.hasProcStateChanged()) { - state.setProcStateChanged(false); - } - int procState = app.mState.getCurProcState(); - if (((procState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND - && procState < ActivityManager.PROCESS_STATE_CACHED_ACTIVITY) - || app.mState.isSystemNoUi()) && app.mProfile.hasPendingUiClean()) { - // If this application is now in the background and it - // had done UI, then give it the special trim level to - // have it free UI resources. - if ((thread = app.getThread()) != null) { - try { - thread.scheduleTrimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); - app.mProfile.setPendingUiClean(false); - } catch (RemoteException e) { - } + // Dispatch UI_HIDDEN to processes that need it + mService.mProcessList.forEachLruProcessesLOSP( + true, + app -> { + final ProcessProfileRecord profile = app.mProfile; + final IApplicationThread thread; + final ProcessStateRecord state = app.mState; + if (state.hasProcStateChanged()) { + state.setProcStateChanged(false); } - } - }); - return false; - } + int procState = app.mState.getCurProcState(); + if (((procState >= ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND + && procState + < ActivityManager.PROCESS_STATE_CACHED_ACTIVITY) + || app.mState.isSystemNoUi()) + && app.mProfile.hasPendingUiClean()) { + // If this application is now in the background and it + // had done UI, then give it the special trim level to + // have it free UI resources. + if ((thread = app.getThread()) != null) { + try { + thread.scheduleTrimMemory( + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN); + app.mProfile.setPendingUiClean(false); + } catch (RemoteException e) { - if (memFactor != ADJ_MEM_FACTOR_NORMAL) { - if (mLowRamStartTime == 0) { - mLowRamStartTime = now; - } - int fgTrimLevel; - switch (memFactor) { - case ADJ_MEM_FACTOR_CRITICAL: - fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; - break; - case ADJ_MEM_FACTOR_LOW: - fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW; - break; - default: - fgTrimLevel = ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE; - break; - } - int factor = numTrimming / 3; - int minFactor = 2; - if (mHasHomeProcess) minFactor++; - if (mHasPreviousProcess) minFactor++; - if (factor < minFactor) factor = minFactor; - final int actualFactor = factor; - final int[] step = {0}; - final int[] curLevel = {ComponentCallbacks2.TRIM_MEMORY_COMPLETE}; - mService.mProcessList.forEachLruProcessesLOSP(true, app -> { - final ProcessProfileRecord profile = app.mProfile; - final int trimMemoryLevel = profile.getTrimMemoryLevel(); - final ProcessStateRecord state = app.mState; - final int curProcState = state.getCurProcState(); - IApplicationThread thread; - if (allChanged || state.hasProcStateChanged()) { - mService.setProcessTrackerStateLOSP(app, trackerMemFactor); - state.setProcStateChanged(false); - } - trimMemoryUiHiddenIfNecessaryLSP(app); - if (curProcState >= ActivityManager.PROCESS_STATE_HOME && !app.isKilledByAm()) { - scheduleTrimMemoryLSP(app, curLevel[0], "Trimming memory of "); - profile.setTrimMemoryLevel(curLevel[0]); - step[0]++; - if (step[0] >= actualFactor) { - step[0] = 0; - switch (curLevel[0]) { - case ComponentCallbacks2.TRIM_MEMORY_COMPLETE: - curLevel[0] = ComponentCallbacks2.TRIM_MEMORY_MODERATE; - break; - case ComponentCallbacks2.TRIM_MEMORY_MODERATE: - curLevel[0] = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; - break; + } } } - } else if (curProcState == ActivityManager.PROCESS_STATE_HEAVY_WEIGHT - && !app.isKilledByAm()) { - scheduleTrimMemoryLSP(app, ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, - "Trimming memory of heavy-weight "); - profile.setTrimMemoryLevel(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND); - } else { - scheduleTrimMemoryLSP(app, fgTrimLevel, "Trimming memory of fg "); - profile.setTrimMemoryLevel(fgTrimLevel); - } - }); - } else { - if (mLowRamStartTime != 0) { - mLowRamTimeSinceLastIdle += now - mLowRamStartTime; - mLowRamStartTime = 0; - } - mService.mProcessList.forEachLruProcessesLOSP(true, app -> { - final ProcessProfileRecord profile = app.mProfile; - final IApplicationThread thread; - final ProcessStateRecord state = app.mState; - if (allChanged || state.hasProcStateChanged()) { - mService.setProcessTrackerStateLOSP(app, trackerMemFactor); - state.setProcStateChanged(false); - } - trimMemoryUiHiddenIfNecessaryLSP(app); - profile.setTrimMemoryLevel(0); - }); - } - return allChanged; + }); } @GuardedBy({"mService", "mProcLock"}) diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 6e20f6cc877d..de27bc708ffd 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1461,8 +1461,7 @@ public final class CachedAppOptimizer { return; } - if (mAm.mConstants.USE_MODERN_TRIM - && app.mState.getSetAdj() >= ProcessList.CACHED_APP_MIN_ADJ) { + if (app.mState.getSetAdj() >= ProcessList.CACHED_APP_MIN_ADJ) { final IApplicationThread thread = app.getThread(); if (thread != null) { try { diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 1a7629f3182d..5e91cd39b698 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -1090,7 +1090,7 @@ public class OomAdjuster { mNumNonCachedProcs = 0; mNumCachedHiddenProcs = 0; - final boolean allChanged = updateAndTrimProcessLSP(now, nowElapsed, oldTime, activeUids, + updateAndTrimProcessLSP(now, nowElapsed, oldTime, activeUids, oomAdjReason, doingAll); mNumServiceProcs = mNewNumServiceProcs; @@ -1100,11 +1100,6 @@ public class OomAdjuster { mService.mAtmInternal.scheduleDestroyAllActivities("always-finish"); } - if (allChanged) { - mService.mAppProfiler.requestPssAllProcsLPr(now, false, - mService.mProcessStats.isMemFactorLowered()); - } - updateUidsLSP(activeUids, nowElapsed); synchronized (mService.mProcessStats.mLock) { @@ -1300,7 +1295,7 @@ public class OomAdjuster { } @GuardedBy({"mService", "mProcLock"}) - private boolean updateAndTrimProcessLSP(final long now, final long nowElapsed, + private void updateAndTrimProcessLSP(final long now, final long nowElapsed, final long oldTime, final ActiveUids activeUids, @OomAdjReason int oomAdjReason, boolean doingAll) { ArrayList<ProcessRecord> lruList = mProcessList.getLruProcessesLOSP(); @@ -1450,7 +1445,7 @@ public class OomAdjuster { mLastFreeSwapPercent = freeSwapPercent; - return mService.mAppProfiler.updateLowMemStateLSP(numCached, numEmpty, numTrimming, now); + mService.mAppProfiler.updateLowMemStateLSP(numCached, numEmpty, numTrimming, now); } @GuardedBy({"mService", "mProcLock"}) diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 9950d8ff42ed..da26209ad495 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -1097,13 +1097,34 @@ public class DisplayDeviceConfig { return mBrightnessToBacklightSpline.interpolate(brightness); } - private float getBrightnessFromBacklight(float brightness) { + /** + * Calculates the screen brightness value - as used among the system from the HAL backlight + * level + * @param backlight value from 0-1 HAL scale + * @return brightness value from 0-1 framework scale + */ + public float getBrightnessFromBacklight(float backlight) { if (mLowBrightnessData != null) { - return mLowBrightnessData.mBacklightToBrightness.interpolate(brightness); + return mLowBrightnessData.mBacklightToBrightness.interpolate(backlight); } - return mBacklightToBrightnessSpline.interpolate(brightness); + return mBacklightToBrightnessSpline.interpolate(backlight); } + /** + * + * @return low brightness mode transition point + */ + public float getLowBrightnessTransitionPoint() { + if (mLowBrightnessData == null) { + return PowerManager.BRIGHTNESS_MIN; + } + return mLowBrightnessData.mTransitionPoint; + } + + /** + * + * @return HAL backlight mapping to framework brightness + */ private Spline getBacklightToBrightnessSpline() { if (mLowBrightnessData != null) { return mLowBrightnessData.mBacklightToBrightness; @@ -1133,7 +1154,12 @@ public class DisplayDeviceConfig { return mBacklightToNitsSpline.interpolate(backlight); } - private float getBacklightFromNits(float nits) { + /** + * + * @param nits - display brightness + * @return corresponding HAL backlight value + */ + public float getBacklightFromNits(float nits) { if (mLowBrightnessData != null) { return mLowBrightnessData.mNitsToBacklight.interpolate(nits); } @@ -1148,6 +1174,18 @@ public class DisplayDeviceConfig { } /** + * + * @param lux - ambient brightness + * @return minimum allowed nits, given the lux. + */ + public float getMinNitsFromLux(float lux) { + if (mLowBrightnessData == null) { + return INVALID_NITS; + } + return mLowBrightnessData.mMinLuxToNits.interpolate(lux); + } + + /** * @return true if there is sdrHdrRatioMap, false otherwise. */ public boolean hasSdrToHdrRatioSpline() { diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 0807cc056a39..d99f7121a14c 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1336,12 +1336,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged() || userSetBrightnessChanged); - mBrightnessRangeController.setAutoBrightnessEnabled( - mAutomaticBrightnessStrategy.isAutoBrightnessEnabled() + final int autoBrightnessState = mAutomaticBrightnessStrategy.isAutoBrightnessEnabled() ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED : mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff() ? AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE - : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED); + : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED; + + mBrightnessRangeController.setAutoBrightnessEnabled(autoBrightnessState); + mBrightnessClamperController.setAutoBrightnessState(autoBrightnessState); boolean updateScreenBrightnessSetting = displayBrightnessState.shouldUpdateScreenBrightnessSetting(); diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index d8a45009f236..9c7504db0cf0 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -66,6 +66,7 @@ public class BrightnessClamperController { private float mCustomAnimationRate = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET; @Nullable private Type mClamperType = null; + private int mAutoBrightnessState = -1; private boolean mClamperApplied = false; @@ -94,7 +95,8 @@ public class BrightnessClamperController { mClampers = injector.getClampers(handler, clamperChangeListenerInternal, data, flags, context); - mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener); + mModifiers = injector.getModifiers(flags, context, handler, clamperChangeListener, + data.mDisplayDeviceConfig); mOnPropertiesChangedListener = properties -> mClampers.forEach(BrightnessClamper::onDeviceConfigChanged); start(); @@ -197,6 +199,19 @@ public class BrightnessClamperController { mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux)); } + /** + * Sets the autobrightness state for clampers that need to be aware of the state. + * @param state autobrightness state + */ + public void setAutoBrightnessState(int state) { + if (state == mAutoBrightnessState) { + return; + } + mModifiers.forEach(modifier -> modifier.setAutoBrightnessState(state)); + mAutoBrightnessState = state; + recalculateBrightnessCap(); + } + // Called in DisplayControllerHandler private void recalculateBrightnessCap() { float brightnessCap = PowerManager.BRIGHTNESS_MAX; @@ -265,12 +280,14 @@ public class BrightnessClamperController { } List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, - Handler handler, ClamperChangeListener listener) { + Handler handler, ClamperChangeListener listener, + DisplayDeviceConfig displayDeviceConfig) { List<BrightnessStateModifier> modifiers = new ArrayList<>(); modifiers.add(new DisplayDimModifier(context)); modifiers.add(new BrightnessLowPowerModeModifier()); if (flags.isEvenDimmerEnabled()) { - modifiers.add(new BrightnessLowLuxModifier(handler, listener, context)); + modifiers.add(new BrightnessLowLuxModifier(handler, listener, context, + displayDeviceConfig)); } return modifiers; } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java index a91bb59b0bc0..7f88c3029820 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java @@ -16,13 +16,14 @@ package com.android.server.display.brightness.clamper; +import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; + import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.hardware.display.DisplayManagerInternal; import android.net.Uri; import android.os.Handler; -import android.os.PowerManager; import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; @@ -30,6 +31,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.utils.DebugUtils; @@ -45,19 +47,23 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot' private static final String TAG = "BrightnessLowLuxModifier"; private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); - private static final float MIN_NITS = 2.0f; + private static final float MIN_NITS_DEFAULT = 0.2f; private final SettingsObserver mSettingsObserver; private final ContentResolver mContentResolver; private final Handler mHandler; private final BrightnessClamperController.ClamperChangeListener mChangeListener; private int mReason; private float mBrightnessLowerBound; + private float mMinNitsAllowed; private boolean mIsActive; + private boolean mAutoBrightnessEnabled; private float mAmbientLux; + private final DisplayDeviceConfig mDisplayDeviceConfig; @VisibleForTesting BrightnessLowLuxModifier(Handler handler, - BrightnessClamperController.ClamperChangeListener listener, Context context) { + BrightnessClamperController.ClamperChangeListener listener, Context context, + DisplayDeviceConfig displayDeviceConfig) { super(); mChangeListener = listener; @@ -67,6 +73,8 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { mHandler.post(() -> { start(); }); + + mDisplayDeviceConfig = displayDeviceConfig; } /** @@ -78,41 +86,45 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { int userId = UserHandle.USER_CURRENT; float settingNitsLowerBound = Settings.Secure.getFloatForUser( mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, - /* def= */ MIN_NITS, userId); + /* def= */ MIN_NITS_DEFAULT, userId); boolean isActive = Settings.Secure.getFloatForUser(mContentResolver, Settings.Secure.EVEN_DIMMER_ACTIVATED, - /* def= */ 0, userId) == 1.0f; - - // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux); - float luxBasedNitsLowerBound = 2.0f; - - final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound, - luxBasedNitsLowerBound) : MIN_NITS; - - final int reason = settingNitsLowerBound > luxBasedNitsLowerBound - ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND - : BrightnessReason.MODIFIER_MIN_LUX; - - // TODO: brightnessLowerBound = nitsToBrightnessSpline(nitsLowerBound); - final float brightnessLowerBound = PowerManager.BRIGHTNESS_MIN; + /* def= */ 0, userId) == 1.0f && mAutoBrightnessEnabled; + + float luxBasedNitsLowerBound = mDisplayDeviceConfig.getMinNitsFromLux(mAmbientLux); + + final int reason; + float minNitsAllowed = -1f; // undefined, if setting is off. + final float minBrightnessAllowed; + + if (isActive) { + minNitsAllowed = Math.max(settingNitsLowerBound, + luxBasedNitsLowerBound); + minBrightnessAllowed = getBrightnessFromNits(minNitsAllowed); + reason = settingNitsLowerBound > luxBasedNitsLowerBound + ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND + : BrightnessReason.MODIFIER_MIN_LUX; + } else { + minBrightnessAllowed = mDisplayDeviceConfig.getLowBrightnessTransitionPoint(); + reason = 0; + } - if (mBrightnessLowerBound != brightnessLowerBound + if (mBrightnessLowerBound != minBrightnessAllowed || mReason != reason || mIsActive != isActive) { mIsActive = isActive; mReason = reason; if (DEBUG) { Slog.i(TAG, "isActive: " + isActive - + ", brightnessLowerBound: " + brightnessLowerBound + + ", minBrightnessAllowed: " + minBrightnessAllowed + ", mAmbientLux: " + mAmbientLux - + ", mReason: " + ( - mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting" - : "lux") - + ", nitsLowerBound: " + nitsLowerBound + + ", mReason: " + (mReason) + + ", minNitsAllowed: " + minNitsAllowed ); } - mBrightnessLowerBound = brightnessLowerBound; + mMinNitsAllowed = minNitsAllowed; + mBrightnessLowerBound = minBrightnessAllowed; mChangeListener.onChanged(); } } @@ -177,11 +189,23 @@ public class BrightnessLowLuxModifier extends BrightnessModifier { } @Override + public void setAutoBrightnessState(int state) { + mAutoBrightnessEnabled = state == AUTO_BRIGHTNESS_ENABLED; + } + + @Override public void dump(PrintWriter pw) { pw.println("BrightnessLowLuxModifier:"); pw.println(" mIsActive=" + mIsActive); pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound); pw.println(" mReason=" + mReason); + pw.println(" mAmbientLux=" + mAmbientLux); + pw.println(" mMinNitsAllowed=" + mMinNitsAllowed); + } + + private float getBrightnessFromNits(float nits) { + return mDisplayDeviceConfig.getBrightnessFromBacklight( + mDisplayDeviceConfig.getBacklightFromNits(nits)); } private final class SettingsObserver extends ContentObserver { diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java index 2a3dd8752615..db5a524da71d 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java @@ -73,4 +73,9 @@ abstract class BrightnessModifier implements BrightnessStateModifier { public void onAmbientLuxChange(float ambientLux) { // do nothing } + + @Override + public void setAutoBrightnessState(int state) { + // do nothing + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java index 22342581fa8b..1606159cb247 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java @@ -48,4 +48,10 @@ public interface BrightnessStateModifier { * @param ambientLux current debounced lux. */ void onAmbientLuxChange(float ambientLux); + + /** + * Sets the autobrightness state for clampers that need to be aware of the state. + * @param state autobrightness state + */ + void setAutoBrightnessState(int state); } diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java index aa82533bf6a7..1a4e807fece6 100644 --- a/services/core/java/com/android/server/display/config/LowBrightnessData.java +++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java @@ -66,11 +66,13 @@ public class LowBrightnessData { * Spline, mapping between backlight and brightness */ public final Spline mBacklightToBrightness; + public final Spline mMinLuxToNits; @VisibleForTesting public LowBrightnessData(float transitionPoint, float[] nits, float[] backlight, float[] brightness, Spline backlightToNits, - Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) { + Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness, + Spline minLuxToNits) { mTransitionPoint = transitionPoint; mNits = nits; mBacklight = backlight; @@ -79,6 +81,7 @@ public class LowBrightnessData { mNitsToBacklight = nitsToBacklight; mBrightnessToBacklight = brightnessToBacklight; mBacklightToBrightness = backlightToBrightness; + mMinLuxToNits = minLuxToNits; } @Override @@ -92,6 +95,7 @@ public class LowBrightnessData { + ", mNitsToBacklight: " + mNitsToBacklight + ", mBrightnessToBacklight: " + mBrightnessToBacklight + ", mBacklightToBrightness: " + mBacklightToBrightness + + ", mMinLuxToNits: " + mMinLuxToNits + "} "; } @@ -132,11 +136,40 @@ public class LowBrightnessData { brightness[i] = brightnessList.get(i); } + final NitsMap map = lbm.getLuxToMinimumNitsMap(); + if (map == null) { + Slog.e(TAG, "Invalid min lux to nits mapping"); + return null; + } + final List<Point> points = map.getPoint(); + final int size = points.size(); + + float[] minLux = new float[size]; + float[] minNits = new float[size]; + + int i = 0; + for (Point point : points) { + minLux[i] = point.getValue().floatValue(); + minNits[i] = point.getNits().floatValue(); + if (i > 0) { + if (minLux[i] < minLux[i - 1]) { + Slog.e(TAG, "minLuxToNitsSpline must be non-decreasing, ignoring rest " + + " of configuration. Value: " + minLux[i] + " < " + minLux[i - 1]); + } + if (minNits[i] < minNits[i - 1]) { + Slog.e(TAG, "minLuxToNitsSpline must be non-decreasing, ignoring rest " + + " of configuration. Nits: " + minNits[i] + " < " + minNits[i - 1]); + } + } + ++i; + } + return new LowBrightnessData(transitionPoints, nits, backlight, brightness, Spline.createSpline(backlight, nits), Spline.createSpline(nits, backlight), Spline.createSpline(brightness, backlight), - Spline.createSpline(backlight, brightness) - ); + Spline.createSpline(backlight, brightness), + Spline.createSpline(minLux, minNits) + ); } } diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java index 0b16af29da28..dbcd21a36ab6 100644 --- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java +++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java @@ -46,29 +46,44 @@ final class LocaleUtils { * @param desired The locale preferred by user. * @return A score based on the locale matching for the default subtype enabling. */ - @IntRange(from = 1, to = 3) + @IntRange(from = 1, to = 4) private static byte calculateMatchingSubScore(@NonNull final ULocale supported, @NonNull final ULocale desired) { // Assuming supported/desired is fully expanded. if (supported.equals(desired)) { - return 3; // Exact match. + return 4; // Exact match. } + // addLikelySubtags is a maximization process as per + // https://www.unicode.org/reports/tr35/#Likely_Subtags + ULocale maxDesired = ULocale.addLikelySubtags(desired); + // Skip language matching since it was already done in calculateMatchingScore. final String supportedScript = supported.getScript(); - if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) { + if (supportedScript.isEmpty() || !supportedScript.equals(maxDesired.getScript())) { // TODO: Need subscript matching. For example, Hanb should match with Bopo. return 1; } final String supportedCountry = supported.getCountry(); - if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) { + if (supportedCountry.isEmpty() || !supportedCountry.equals(maxDesired.getCountry())) { return 2; } // Ignore others e.g. variants, extensions. - return 3; + + // Since addLikelySubtags can canonicalize subtags, e.g. the deprecated country codes + // an locale with an identical script and country before addLikelySubtags is in favour, + // and a score of 4 is returned. + String desiredScript = desired.getScript(); + String desiredCountry = desired.getCountry(); + if ((desiredScript.isEmpty() || desiredScript.equals(maxDesired.getScript())) + && (desiredCountry.isEmpty() || desiredCountry.equals(maxDesired.getCountry()))) { + return 4; + } else { + return 3; + } } private static final class ScoreEntry implements Comparable<ScoreEntry> { @@ -180,8 +195,7 @@ final class LocaleUtils { ULocale.forLocale(preferredLocale)); } score[j] = calculateMatchingSubScore( - preferredULocaleCache[j], - ULocale.addLikelySubtags(ULocale.forLocale(locale))); + preferredULocaleCache[j], ULocale.forLocale(locale)); if (canSkip && score[j] != 0) { canSkip = false; } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index ae6a7e968b0f..ec15ff35676c 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -221,18 +221,27 @@ class MediaRouter2ServiceImpl { // Start of methods that implement MediaRouter2 operations. @NonNull - public List<MediaRoute2Info> getSystemRoutes() { + public List<MediaRoute2Info> getSystemRoutes(@NonNull String callerPackageName, + boolean isProxyRouter) { final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); - final boolean hasSystemRoutingPermission = checkCallerHasSystemRoutingPermissions(pid, uid); + + boolean hasSystemRoutingPermissions; + if (!isProxyRouter) { + hasSystemRoutingPermissions = checkCallerHasSystemRoutingPermissions(pid, uid); + } else { + // Request from ProxyRouter. + hasSystemRoutingPermissions = + checkCallerHasPrivilegedRoutingPermissions(pid, uid, callerPackageName); + } final long token = Binder.clearCallingIdentity(); try { Collection<MediaRoute2Info> systemRoutes; synchronized (mLock) { UserRecord userRecord = getOrCreateUserRecordLocked(userId); - if (hasSystemRoutingPermission) { + if (hasSystemRoutingPermissions) { MediaRoute2ProviderInfo providerInfo = userRecord.mHandler.mSystemProvider.getProviderInfo(); if (providerInfo != null) { @@ -795,12 +804,21 @@ class MediaRouter2ServiceImpl { @Nullable public RoutingSessionInfo getSystemSessionInfo( - @Nullable String packageName, boolean setDeviceRouteSelected) { + @NonNull String callerPackageName, + @Nullable String targetPackageName, + boolean setDeviceRouteSelected) { final int uid = Binder.getCallingUid(); final int pid = Binder.getCallingPid(); final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); - final boolean hasSystemRoutingPermissions = - checkCallerHasSystemRoutingPermissions(pid, uid); + + boolean hasSystemRoutingPermissions; + if (targetPackageName == null) { + hasSystemRoutingPermissions = checkCallerHasSystemRoutingPermissions(pid, uid); + } else { + // Request from ProxyRouter. + hasSystemRoutingPermissions = + checkCallerHasPrivilegedRoutingPermissions(pid, uid, callerPackageName); + } final long token = Binder.clearCallingIdentity(); try { @@ -812,14 +830,14 @@ class MediaRouter2ServiceImpl { // Return a fake system session that shows the device route as selected and // available bluetooth routes as transferable. return userRecord.mHandler.mSystemProvider - .generateDeviceRouteSelectedSessionInfo(packageName); + .generateDeviceRouteSelectedSessionInfo(targetPackageName); } else { sessionInfos = userRecord.mHandler.mSystemProvider.getSessionInfos(); if (!sessionInfos.isEmpty()) { // Return a copy of the current system session with no modification, // except setting the client package name. return new RoutingSessionInfo.Builder(sessionInfos.get(0)) - .setClientPackageName(packageName) + .setClientPackageName(targetPackageName) .build(); } else { Slog.w(TAG, "System provider does not have any session info."); @@ -828,7 +846,7 @@ class MediaRouter2ServiceImpl { } else { return new RoutingSessionInfo.Builder( userRecord.mHandler.mSystemProvider.getDefaultSessionInfo()) - .setClientPackageName(packageName) + .setClientPackageName(targetPackageName) .build(); } } @@ -843,6 +861,12 @@ class MediaRouter2ServiceImpl { || checkCallerHasBluetoothPermissions(pid, uid); } + private boolean checkCallerHasPrivilegedRoutingPermissions( + int pid, int uid, @NonNull String callerPackageName) { + return checkMediaContentControlPermission(uid, pid) + || checkMediaRoutingControlPermission(uid, pid, callerPackageName); + } + private boolean checkCallerHasModifyAudioRoutingPermission(int pid, int uid) { return mContext.checkPermission(Manifest.permission.MODIFY_AUDIO_ROUTING, pid, uid) == PackageManager.PERMISSION_GRANTED; @@ -864,30 +888,29 @@ class MediaRouter2ServiceImpl { Manifest.permission.MEDIA_CONTENT_CONTROL }) private void enforcePrivilegedRoutingPermissions( - int callerUid, int callerPid, @Nullable String callerPackageName) { - if (hasMediaContentControlPermission(callerUid, callerPid)) { + int callerUid, int callerPid, @NonNull String callerPackageName) { + if (checkMediaContentControlPermission(callerUid, callerPid)) { return; } - if (!Flags.enablePrivilegedRoutingForMediaRoutingControl()) { - throw new SecurityException("Must hold MEDIA_CONTENT_CONTROL"); - } - if (!checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName)) { throw new SecurityException( "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions."); } } - @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) - private boolean hasMediaContentControlPermission(int callerUid, int callerPid) { + private boolean checkMediaContentControlPermission(int callerUid, int callerPid) { return mContext.checkPermission( Manifest.permission.MEDIA_CONTENT_CONTROL, callerPid, callerUid) == PackageManager.PERMISSION_GRANTED; } private boolean checkMediaRoutingControlPermission( - int callerUid, int callerPid, @Nullable String callerPackageName) { + int callerUid, int callerPid, @NonNull String callerPackageName) { + if (!Flags.enablePrivilegedRoutingForMediaRoutingControl()) { + return false; + } + return PermissionChecker.checkPermissionForDataDelivery( mContext, Manifest.permission.MEDIA_ROUTING_CONTROL, @@ -1520,7 +1543,7 @@ class MediaRouter2ServiceImpl { boolean hasMediaRoutingControl = checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName); - boolean hasMediaContentControl = hasMediaContentControlPermission(callerUid, callerPid); + boolean hasMediaContentControl = checkMediaContentControlPermission(callerUid, callerPid); Slog.i( TAG, diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 6af3480989fa..76b8db685f52 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -411,15 +411,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public List<MediaRoute2Info> getSystemRoutes() { - return mService2.getSystemRoutes(); + public List<MediaRoute2Info> getSystemRoutes(@NonNull String callerPackageName, + boolean isProxyRouter) { + if (!validatePackageName(Binder.getCallingUid(), callerPackageName)) { + throw new SecurityException("callerPackageName does not match calling uid."); + } + return mService2.getSystemRoutes(callerPackageName, isProxyRouter); } // Binder call @Override public RoutingSessionInfo getSystemSessionInfo() { return mService2.getSystemSessionInfo( - null /* packageName */, false /* setDeviceRouteSelected */); + /* callerPackageName */ null, + /* targetPackageName */ null, /* setDeviceRouteSelected */ + false); } // Binder call @@ -530,16 +536,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public RoutingSessionInfo getSystemSessionInfoForPackage(@Nullable String packageName) { + public RoutingSessionInfo getSystemSessionInfoForPackage( + @NonNull String callerPackageName, @Nullable String targetPackageName) { final int uid = Binder.getCallingUid(); final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); + + if (!validatePackageName(uid, callerPackageName)) { + throw new SecurityException("callerPackageName does not match calling uid."); + } + boolean setDeviceRouteSelected = false; synchronized (mLock) { UserRecord userRecord = mUserRecords.get(userId); List<ClientRecord> userClientRecords = userRecord != null ? userRecord.mClientRecords : Collections.emptyList(); for (ClientRecord clientRecord : userClientRecords) { - if (TextUtils.equals(clientRecord.mPackageName, packageName)) { + if (TextUtils.equals(clientRecord.mPackageName, targetPackageName)) { if (mDefaultAudioRouteId.equals(clientRecord.mSelectedRouteId)) { setDeviceRouteSelected = true; break; @@ -547,7 +559,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub } } } - return mService2.getSystemSessionInfo(packageName, setDeviceRouteSelected); + return mService2.getSystemSessionInfo( + callerPackageName, targetPackageName, setDeviceRouteSelected); } // Binder call diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index ce31ac84cbe3..eb4e6e44c8a4 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -431,28 +431,9 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde int stream = getVolumeStream(mAudioAttrs); final int volumeValue = value; mHandler.post( - new Runnable() { - @Override - public void run() { - try { - mAudioManager.setStreamVolumeForUid( - stream, - volumeValue, - flags, - opPackageName, - uid, - pid, - mContext.getApplicationInfo().targetSdkVersion); - } catch (IllegalArgumentException | SecurityException e) { - Slog.e( - TAG, - "Cannot set volume: stream=" + stream - + ", value=" + volumeValue - + ", flags=" + flags, - e); - } - } - }); + () -> + setStreamVolumeForUid( + opPackageName, pid, uid, flags, stream, volumeValue)); } else { if (mVolumeControlType != VOLUME_CONTROL_ABSOLUTE) { if (DEBUG) { @@ -482,6 +463,27 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } + private void setStreamVolumeForUid( + String opPackageName, int pid, int uid, int flags, int stream, int volumeValue) { + try { + mAudioManager.setStreamVolumeForUid( + stream, + volumeValue, + flags, + opPackageName, + uid, + pid, + mContext.getApplicationInfo().targetSdkVersion); + } catch (IllegalArgumentException | SecurityException e) { + Slog.e( + TAG, + "Cannot set volume: stream=" + stream + + ", value=" + volumeValue + + ", flags=" + flags, + e); + } + } + /** * Check if this session has been set to active by the app. * <p> @@ -749,52 +751,70 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde pid = callingPid; } mHandler.post( - new Runnable() { - @Override - public void run() { - try { - if (useSuggested) { - if (AudioSystem.isStreamActive(stream, 0)) { - mAudioManager.adjustSuggestedStreamVolumeForUid( - stream, - direction, - flags, - opPackageName, - uid, - pid, - mContext.getApplicationInfo().targetSdkVersion); - } else { - mAudioManager.adjustSuggestedStreamVolumeForUid( - AudioManager.USE_DEFAULT_STREAM_TYPE, - direction, - flags | previousFlagPlaySound, - opPackageName, - uid, - pid, - mContext.getApplicationInfo().targetSdkVersion); - } - } else { - mAudioManager.adjustStreamVolumeForUid( - stream, - direction, - flags, - opPackageName, - uid, - pid, - mContext.getApplicationInfo().targetSdkVersion); - } - } catch (IllegalArgumentException | SecurityException e) { - Slog.e( - TAG, - "Cannot adjust volume: direction=" + direction - + ", stream=" + stream + ", flags=" + flags - + ", opPackageName=" + opPackageName + ", uid=" + uid - + ", useSuggested=" + useSuggested - + ", previousFlagPlaySound=" + previousFlagPlaySound, - e); - } - } - }); + () -> + adjustSuggestedStreamVolumeForUid( + stream, + direction, + flags, + useSuggested, + previousFlagPlaySound, + opPackageName, + uid, + pid)); + } + + private void adjustSuggestedStreamVolumeForUid( + int stream, + int direction, + int flags, + boolean useSuggested, + int previousFlagPlaySound, + String opPackageName, + int uid, + int pid) { + try { + if (useSuggested) { + if (AudioSystem.isStreamActive(stream, 0)) { + mAudioManager.adjustSuggestedStreamVolumeForUid( + stream, + direction, + flags, + opPackageName, + uid, + pid, + mContext.getApplicationInfo().targetSdkVersion); + } else { + mAudioManager.adjustSuggestedStreamVolumeForUid( + AudioManager.USE_DEFAULT_STREAM_TYPE, + direction, + flags | previousFlagPlaySound, + opPackageName, + uid, + pid, + mContext.getApplicationInfo().targetSdkVersion); + } + } else { + mAudioManager.adjustStreamVolumeForUid( + stream, + direction, + flags, + opPackageName, + uid, + pid, + mContext.getApplicationInfo().targetSdkVersion); + } + } catch (IllegalArgumentException | SecurityException e) { + Slog.e( + TAG, + "Cannot adjust volume: direction=" + direction + + ", stream=" + stream + + ", flags=" + flags + + ", opPackageName=" + opPackageName + + ", uid=" + uid + + ", useSuggested=" + useSuggested + + ", previousFlagPlaySound=" + previousFlagPlaySound, + e); + } } private void logCallbackException( @@ -1089,16 +1109,14 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde volumeType, VOLUME_CONTROL_ABSOLUTE, max, current, attributes, null); } - private final Runnable mClearOptimisticVolumeRunnable = new Runnable() { - @Override - public void run() { - boolean needUpdate = (mOptimisticVolume != mCurrentVolume); - mOptimisticVolume = -1; - if (needUpdate) { - pushVolumeUpdate(); - } - } - }; + private final Runnable mClearOptimisticVolumeRunnable = + () -> { + boolean needUpdate = (mOptimisticVolume != mCurrentVolume); + mOptimisticVolume = -1; + if (needUpdate) { + pushVolumeUpdate(); + } + }; @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS) private static boolean componentNameExists( diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java index 3ecc58e2aef2..e1e2b3efd1eb 100644 --- a/services/core/java/com/android/server/notification/ManagedServices.java +++ b/services/core/java/com/android/server/notification/ManagedServices.java @@ -25,6 +25,8 @@ import static android.os.UserHandle.USER_ALL; import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; +import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; + import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.app.ActivityManager; @@ -1433,7 +1435,7 @@ abstract public class ManagedServices { protected void rebindServices(boolean forceRebind, int userToRebind) { if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind); IntArray userIds = mUserProfiles.getCurrentProfileIds(); - boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind) + boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext) && allowRebindForParentUser(); if (userToRebind != USER_ALL && !rebindAllCurrentUsers) { userIds = new IntArray(1); @@ -1958,7 +1960,7 @@ abstract public class ManagedServices { * from receiving events from the profile. */ public boolean isPermittedForProfile(int userId) { - if (!mUserProfiles.isProfileUser(userId)) { + if (!mUserProfiles.isProfileUser(userId, mContext)) { return true; } DevicePolicyManager dpm = @@ -2036,16 +2038,26 @@ abstract public class ManagedServices { } } - public boolean isProfileUser(int userId) { + public boolean isProfileUser(int userId, Context context) { synchronized (mCurrentProfiles) { UserInfo user = mCurrentProfiles.get(userId); if (user == null) { return false; } - if (user.isManagedProfile() || user.isCloneProfile()) { - return true; + if (privateSpaceFlagsEnabled()) { + return user.isProfile() && hasParent(user, context); } - return false; + return user.isManagedProfile() || user.isCloneProfile(); + } + } + + boolean hasParent(UserInfo profile, Context context) { + final long identity = Binder.clearCallingIdentity(); + try { + UserManager um = context.getSystemService(UserManager.class); + return um.getProfileParent(profile.id) != null; + } finally { + Binder.restoreCallingIdentity(identity); } } } diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 097daf2e51e6..5563caee3743 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import static android.app.Flags.updateRankingTime; import static android.app.Notification.FLAG_INSISTENT; import static android.app.Notification.FLAG_ONLY_ALERT_ONCE; import static android.app.NotificationManager.IMPORTANCE_MIN; @@ -496,6 +497,11 @@ public final class NotificationAttentionHelper { Slog.v(TAG, "INTERRUPTIVENESS: " + record.getKey() + " is interruptive: alerted"); } + if (updateRankingTime()) { + if (buzz || beep) { + record.resetRankingTime(); + } + } } } final int buzzBeepBlinkLoggingCode = diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9fcdfdd564b6..f48f66fae404 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -142,6 +142,7 @@ import static android.service.notification.NotificationListenerService.TRIM_LIGH import static android.view.contentprotection.flags.Flags.rapidClearNotificationsByListenerAppOpEnabled; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; +import static android.app.Flags.updateRankingTime; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES; @@ -238,9 +239,6 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.drawable.Icon; import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.AudioManagerInternal; -import android.media.IRingtonePlayer; import android.metrics.LogMaker; import android.net.Uri; import android.os.Binder; @@ -612,8 +610,7 @@ public class NotificationManagerService extends SystemService { PackageManagerInternal mPackageManagerInternal; private PermissionManager mPermissionManager; private PermissionPolicyInternal mPermissionPolicyInternal; - AudioManager mAudioManager; - AudioManagerInternal mAudioManagerInternal; + // Can be null for wear @Nullable StatusBarManagerInternal mStatusBar; private WindowManagerInternal mWindowManagerInternal; @@ -641,34 +638,12 @@ public class NotificationManagerService extends SystemService { private final HandlerThread mRankingThread = new HandlerThread("ranker", Process.THREAD_PRIORITY_BACKGROUND); - private LogicalLight mNotificationLight; - LogicalLight mAttentionLight; - - private boolean mUseAttentionLight; - boolean mHasLight = true; - boolean mSystemReady; - - private boolean mDisableNotificationEffects; - private int mCallState; - private String mSoundNotificationKey; - private String mVibrateNotificationKey; - private final SparseArray<ArraySet<ComponentName>> mListenersDisablingEffects = new SparseArray<>(); private List<ComponentName> mEffectsSuppressors = new ArrayList<>(); private int mListenerHints; // right now, all hints are global private int mInterruptionFilter = NotificationListenerService.INTERRUPTION_FILTER_UNKNOWN; - // for enabling and disabling notification pulse behavior - boolean mScreenOn = true; - protected boolean mInCallStateOffHook = false; - boolean mNotificationPulseEnabled; - - private Uri mInCallNotificationUri; - private AudioAttributes mInCallNotificationAudioAttributes; - private float mInCallNotificationVolume; - private Binder mCallNotificationToken = null; - private SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver; // used as a mutex for access to all active notifications & listeners @@ -696,11 +671,6 @@ public class NotificationManagerService extends SystemService { // Used for rate limiting toasts by package. private MultiRateLimiter mToastRateLimiter; - private KeyguardManager mKeyguardManager; - - // The last key in this list owns the hardware. - ArrayList<String> mLights = new ArrayList<>(); - private AppOpsManager mAppOps; private UsageStatsManagerInternal mAppUsageStats; private DevicePolicyManagerInternal mDpm; @@ -725,7 +695,6 @@ public class NotificationManagerService extends SystemService { RankingHelper mRankingHelper; @VisibleForTesting PreferencesHelper mPreferencesHelper; - private VibratorHelper mVibratorHelper; private final UserProfiles mUserProfiles = new UserProfiles(); private NotificationListeners mListeners; @@ -751,8 +720,6 @@ public class NotificationManagerService extends SystemService { private GroupHelper mGroupHelper; private int mAutoGroupAtCount; private boolean mIsTelevision; - private boolean mIsAutomotive; - private boolean mNotificationEffectsEnabledForAutomotive; private DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener; protected NotificationAttentionHelper mAttentionHelper; @@ -957,8 +924,7 @@ public class NotificationManagerService extends SystemService { final List<UserInfo> activeUsers = mUm.getUsers(); for (UserInfo userInfo : activeUsers) { int userId = userInfo.getUserHandle().getIdentifier(); - if (isNASMigrationDone(userId) - || userInfo.isManagedProfile() || userInfo.isCloneProfile()) { + if (isNASMigrationDone(userId) || isProfileUser(userInfo)) { continue; } List<ComponentName> allowedComponents = mAssistants.getAllowedComponents(userId); @@ -989,6 +955,17 @@ public class NotificationManagerService extends SystemService { Settings.Secure.NAS_SETTINGS_UPDATED, 0, userId) == 1); } + boolean isProfileUser(UserInfo userInfo) { + if (privateSpaceFlagsEnabled()) { + return userInfo.isProfile() && hasParent(userInfo); + } + return userInfo.isManagedProfile() || userInfo.isCloneProfile(); + } + + boolean hasParent(UserInfo profile) { + return mUmInternal.getProfileParentId(profile.id) != profile.id; + } + protected void setDefaultAssistantForUser(int userId) { String overrideDefaultAssistantString = DeviceConfig.getProperty( DeviceConfig.NAMESPACE_SYSTEMUI, @@ -1097,8 +1074,7 @@ public class NotificationManagerService extends SystemService { XmlUtils.beginDocument(parser, TAG_NOTIFICATION_POLICY); boolean migratedManagedServices = false; UserInfo userInfo = mUmInternal.getUserInfo(userId); - boolean ineligibleForManagedServices = forRestore && - (userInfo.isManagedProfile() || userInfo.isCloneProfile()); + boolean ineligibleForManagedServices = forRestore && isProfileUser(userInfo); int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (ZenModeConfig.ZEN_TAG.equals(parser.getName())) { @@ -1205,7 +1181,7 @@ public class NotificationManagerService extends SystemService { } } - private static boolean privateSpaceFlagsEnabled() { + protected static boolean privateSpaceFlagsEnabled() { return allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures(); } @@ -1270,17 +1246,7 @@ public class NotificationManagerService extends SystemService { @Override public void onSetDisabled(int status) { synchronized (mNotificationLock) { - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.updateDisableNotificationEffectsLocked(status); - } else { - mDisableNotificationEffects = - (status & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0; - if (disableNotificationEffects(null) != null) { - // cancel whatever's going on - clearSoundLocked(); - clearVibrateLocked(); - } - } + mAttentionHelper.updateDisableNotificationEffectsLocked(status); } } @@ -1421,13 +1387,7 @@ public class NotificationManagerService extends SystemService { public void clearEffects() { synchronized (mNotificationLock) { if (DBG) Slog.d(TAG, "clearEffects"); - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.clearAttentionEffects(); - } else { - clearSoundLocked(); - clearVibrateLocked(); - clearLightsLocked(); - } + mAttentionHelper.clearAttentionEffects(); } } @@ -1695,11 +1655,7 @@ public class NotificationManagerService extends SystemService { int changedFlags = data.getFlags() ^ flags; if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) { // Suppress notification flag changed, clear any effects - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.clearEffectsLocked(key); - } else { - clearEffectsLocked(key); - } + mAttentionHelper.clearEffectsLocked(key); } data.setFlags(flags); // Shouldn't alert again just because of a flag change. @@ -1832,53 +1788,6 @@ public class NotificationManagerService extends SystemService { hasSensitiveContent, lifespanMs); } - @GuardedBy("mNotificationLock") - void clearSoundLocked() { - mSoundNotificationKey = null; - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - player.stopAsync(); - } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - @GuardedBy("mNotificationLock") - void clearVibrateLocked() { - mVibrateNotificationKey = null; - final long identity = Binder.clearCallingIdentity(); - try { - mVibratorHelper.cancelVibration(); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - @GuardedBy("mNotificationLock") - private void clearLightsLocked() { - // light - mLights.clear(); - updateLightsLocked(); - } - - @GuardedBy("mNotificationLock") - private void clearEffectsLocked(String key) { - if (key.equals(mSoundNotificationKey)) { - clearSoundLocked(); - } - if (key.equals(mVibrateNotificationKey)) { - clearVibrateLocked(); - } - boolean removed = mLights.remove(key); - if (removed) { - updateLightsLocked(); - } - } - protected final BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -2068,27 +1977,6 @@ public class NotificationManagerService extends SystemService { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (!Flags.refactorAttentionHelper()) { - if (action.equals(Intent.ACTION_SCREEN_ON)) { - // Keep track of screen on/off state, but do not turn off the notification light - // until user passes through the lock screen or views the notification. - mScreenOn = true; - updateNotificationPulse(); - } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { - mScreenOn = false; - updateNotificationPulse(); - } else if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) { - mInCallStateOffHook = TelephonyManager.EXTRA_STATE_OFFHOOK - .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE)); - updateNotificationPulse(); - } else if (action.equals(Intent.ACTION_USER_PRESENT)) { - // turn off LED when user passes through lock screen - if (mNotificationLight != null) { - mNotificationLight.turnOff(); - } - } - } - if (action.equals(Intent.ACTION_USER_STOPPED)) { int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (userHandle >= 0) { @@ -2106,7 +1994,7 @@ public class NotificationManagerService extends SystemService { } else if (action.equals(Intent.ACTION_USER_SWITCHED)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); mUserProfiles.updateCache(context); - if (!mUserProfiles.isProfileUser(userId)) { + if (!mUserProfiles.isProfileUser(userId, context)) { // reload per-user settings mSettingsObserver.update(null); // Refresh managed services @@ -2121,7 +2009,7 @@ public class NotificationManagerService extends SystemService { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); if (userId != USER_NULL) { mUserProfiles.updateCache(context); - if (!mUserProfiles.isProfileUser(userId)) { + if (!mUserProfiles.isProfileUser(userId, context)) { allowDefaultApprovedServices(userId); } mHistoryManager.onUserAdded(userId); @@ -2142,7 +2030,7 @@ public class NotificationManagerService extends SystemService { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL); mUserProfiles.updateCache(context); mAssistants.onUserUnlocked(userId); - if (!mUserProfiles.isProfileUser(userId)) { + if (!mUserProfiles.isProfileUser(userId, context)) { mConditionProviders.onUserUnlocked(userId); mListeners.onUserUnlocked(userId); if (!android.app.Flags.modesApi()) { @@ -2164,8 +2052,6 @@ public class NotificationManagerService extends SystemService { = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BADGING); private final Uri NOTIFICATION_BUBBLES_URI = Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES); - private final Uri NOTIFICATION_LIGHT_PULSE_URI - = Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE); private final Uri NOTIFICATION_RATE_LIMIT_URI = Settings.Global.getUriFor(Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE); private final Uri NOTIFICATION_HISTORY_ENABLED @@ -2188,10 +2074,6 @@ public class NotificationManagerService extends SystemService { ContentResolver resolver = getContext().getContentResolver(); resolver.registerContentObserver(NOTIFICATION_BADGING_URI, false, this, UserHandle.USER_ALL); - if (!Flags.refactorAttentionHelper()) { - resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI, - false, this, UserHandle.USER_ALL); - } resolver.registerContentObserver(NOTIFICATION_RATE_LIMIT_URI, false, this, UserHandle.USER_ALL); resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI, @@ -2218,17 +2100,6 @@ public class NotificationManagerService extends SystemService { public void update(Uri uri) { ContentResolver resolver = getContext().getContentResolver(); - if (!Flags.refactorAttentionHelper()) { - if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) { - boolean pulseEnabled = Settings.System.getIntForUser(resolver, - Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT) - != 0; - if (mNotificationPulseEnabled != pulseEnabled) { - mNotificationPulseEnabled = pulseEnabled; - updateNotificationPulse(); - } - } - } if (uri == null || NOTIFICATION_RATE_LIMIT_URI.equals(uri)) { mMaxPackageEnqueueRate = Settings.Global.getFloat(resolver, Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, mMaxPackageEnqueueRate); @@ -2347,21 +2218,11 @@ public class NotificationManagerService extends SystemService { // TODO - replace these methods with new fields in the VisibleForTesting constructor @VisibleForTesting - void setAudioManager(AudioManager audioManager) { - mAudioManager = audioManager; - } - - @VisibleForTesting void setStrongAuthTracker(StrongAuthTracker strongAuthTracker) { mStrongAuthTracker = strongAuthTracker; } @VisibleForTesting - void setKeyguardManager(KeyguardManager keyguardManager) { - mKeyguardManager = keyguardManager; - } - - @VisibleForTesting ShortcutHelper getShortcutHelper() { return mShortcutHelper; } @@ -2372,33 +2233,6 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting - VibratorHelper getVibratorHelper() { - return mVibratorHelper; - } - - @VisibleForTesting - void setVibratorHelper(VibratorHelper helper) { - mVibratorHelper = helper; - } - - @VisibleForTesting - void setHints(int hints) { - mListenerHints = hints; - } - - @VisibleForTesting - void setLights(LogicalLight light) { - mNotificationLight = light; - mAttentionLight = light; - mNotificationPulseEnabled = true; - } - - @VisibleForTesting - void setScreenOn(boolean on) { - mScreenOn = on; - } - - @VisibleForTesting int getNotificationRecordCount() { synchronized (mNotificationLock) { int count = mNotificationList.size() + mNotificationsByKey.size() @@ -2446,12 +2280,6 @@ public class NotificationManagerService extends SystemService { return mNotificationsByKey.get(key); } - - @VisibleForTesting - void setSystemReady(boolean systemReady) { - mSystemReady = systemReady; - } - @VisibleForTesting void setHandler(WorkerHandler handler) { mHandler = handler; @@ -2471,13 +2299,8 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting - void setIsAutomotive(boolean isAutomotive) { - mIsAutomotive = isAutomotive; - } - - @VisibleForTesting - void setNotificationEffectsEnabledForAutomotive(boolean isEnabled) { - mNotificationEffectsEnabledForAutomotive = isEnabled; + void setAttentionHelper(NotificationAttentionHelper nah) { + mAttentionHelper = nah; } @VisibleForTesting @@ -2486,16 +2309,6 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting - void setUsageStats(NotificationUsageStats us) { - mUsageStats = us; - } - - @VisibleForTesting - void setAccessibilityManager(AccessibilityManager am) { - mAccessibilityManager = am; - } - - @VisibleForTesting void setTelecomManager(TelecomManager tm) { mTelecomManager = tm; } @@ -2513,7 +2326,7 @@ public class NotificationManagerService extends SystemService { DevicePolicyManagerInternal dpm, IUriGrantsManager ugm, UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager, NotificationHistoryManager historyManager, StatsManager statsManager, - TelephonyManager telephonyManager, ActivityManagerInternal ami, + ActivityManagerInternal ami, MultiRateLimiter toastRateLimiter, PermissionHelper permissionHelper, UsageStatsManagerInternal usageStatsManagerInternal, TelecomManager telecomManager, NotificationChannelLogger channelLogger, @@ -2645,7 +2458,6 @@ public class NotificationManagerService extends SystemService { extractorNames); mSnoozeHelper = snoozeHelper; mGroupHelper = groupHelper; - mVibratorHelper = new VibratorHelper(getContext()); mHistoryManager = historyManager; // This is a ManagedServices object that keeps track of the listeners. @@ -2664,43 +2476,9 @@ public class NotificationManagerService extends SystemService { mStatusBar.setNotificationDelegate(mNotificationDelegate); } - mNotificationLight = lightsManager.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS); - mAttentionLight = lightsManager.getLight(LightsManager.LIGHT_ID_ATTENTION); - - mInCallNotificationUri = Uri.parse("file://" + - resources.getString(R.string.config_inCallNotificationSound)); - mInCallNotificationAudioAttributes = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .build(); - mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume); - - mUseAttentionLight = resources.getBoolean(R.bool.config_useAttentionLight); - mHasLight = - resources.getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed); - - // Don't start allowing notifications until the setup wizard has run once. - // After that, including subsequent boots, init with notifications turned on. - // This works on the first boot because the setup wizard will toggle this - // flag at least once and we'll go back to 0 after that. - if (0 == Settings.Global.getInt(getContext().getContentResolver(), - Settings.Global.DEVICE_PROVISIONED, 0)) { - mDisableNotificationEffects = true; - } mZenModeHelper.initZenMode(); mInterruptionFilter = mZenModeHelper.getZenModeListenerInterruptionFilter(); - if (mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { - telephonyManager.listen(new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String incomingNumber) { - if (mCallState == state) return; - if (DBG) Slog.d(TAG, "Call state changed: " + callStateToString(state)); - mCallState = state; - } - }, PhoneStateListener.LISTEN_CALL_STATE); - } - mSettingsObserver = new SettingsObserver(mHandler); mArchive = new Archive(resources.getInteger( @@ -2709,11 +2487,6 @@ public class NotificationManagerService extends SystemService { mIsTelevision = mPackageManagerClient.hasSystemFeature(FEATURE_LEANBACK) || mPackageManagerClient.hasSystemFeature(FEATURE_TELEVISION); - mIsAutomotive = - mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0); - mNotificationEffectsEnabledForAutomotive = - resources.getBoolean(R.bool.config_enableServerNotificationEffectsForAutomotive); - mZenModeHelper.setPriorityOnlyDndExemptPackages(getContext().getResources().getStringArray( com.android.internal.R.array.config_priorityOnlyDndExemptPackages)); @@ -2733,22 +2506,14 @@ public class NotificationManagerService extends SystemService { mToastRateLimiter = toastRateLimiter; - if (Flags.refactorAttentionHelper()) { - mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager, + mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager, mAccessibilityManager, mPackageManagerClient, userManager, usageStats, mNotificationManagerPrivate, mZenModeHelper, flagResolver); - } // register for various Intents. // If this is called within a test, make sure to unregister the intent receivers by // calling onDestroy() IntentFilter filter = new IntentFilter(); - if (!Flags.refactorAttentionHelper()) { - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); - filter.addAction(Intent.ACTION_USER_PRESENT); - } filter.addAction(Intent.ACTION_USER_STOPPED); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_USER_ADDED); @@ -2874,7 +2639,6 @@ public class NotificationManagerService extends SystemService { new NotificationHistoryManager(getContext(), handler), mStatsManager = (StatsManager) getContext().getSystemService( Context.STATS_MANAGER), - getContext().getSystemService(TelephonyManager.class), LocalServices.getService(ActivityManagerInternal.class), createToastRateLimiter(), new PermissionHelper(getContext(), AppGlobals.getPackageManager(), @@ -3054,14 +2818,7 @@ public class NotificationManagerService extends SystemService { @VisibleForTesting void onBootPhase(int phase, Looper mainLooper) { if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) { - // no beeping until we're basically done booting - mSystemReady = true; - - // Grab our optional AudioService - mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); - mAudioManagerInternal = getLocalService(AudioManagerInternal.class); mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); - mKeyguardManager = getContext().getSystemService(KeyguardManager.class); mZenModeHelper.onSystemReady(); RoleObserver roleObserver = new RoleObserver(getContext(), getContext().getSystemService(RoleManager.class), @@ -3080,9 +2837,7 @@ public class NotificationManagerService extends SystemService { } registerNotificationPreferencesPullers(); new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker); - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.onSystemReady(); - } + mAttentionHelper.onSystemReady(); } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { // This observer will force an update when observe is called, causing us to // bind to listener services. @@ -6866,33 +6621,6 @@ public class NotificationManagerService extends SystemService { return null; } - private String disableNotificationEffects(NotificationRecord record) { - if (mDisableNotificationEffects) { - return "booleanState"; - } - if ((mListenerHints & HINT_HOST_DISABLE_EFFECTS) != 0) { - return "listenerHints"; - } - if (record != null && record.getAudioAttributes() != null) { - if ((mListenerHints & HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) != 0) { - if (record.getAudioAttributes().getUsage() - != AudioAttributes.USAGE_NOTIFICATION_RINGTONE) { - return "listenerNoti"; - } - } - if ((mListenerHints & HINT_HOST_DISABLE_CALL_EFFECTS) != 0) { - if (record.getAudioAttributes().getUsage() - == AudioAttributes.USAGE_NOTIFICATION_RINGTONE) { - return "listenerCall"; - } - } - } - if (mCallState != TelephonyManager.CALL_STATE_IDLE && !mZenModeHelper.isCall(record)) { - return "callState"; - } - return null; - } - // Gets packages that have requested notification permission, and whether that has been // allowed/denied, for all users on the device. // Returns a single map containing that info keyed by (uid, package name) for all users. @@ -7061,33 +6789,10 @@ public class NotificationManagerService extends SystemService { dumpNotificationRecords(pw, filter); } if (!filter.filtered) { - N = mLights.size(); - if (N > 0) { - pw.println(" Lights List:"); - for (int i=0; i<N; i++) { - if (i == N - 1) { - pw.print(" > "); - } else { - pw.print(" "); - } - pw.println(mLights.get(i)); - } - pw.println(" "); - } - pw.println(" mUseAttentionLight=" + mUseAttentionLight); - pw.println(" mHasLight=" + mHasLight); - pw.println(" mNotificationPulseEnabled=" + mNotificationPulseEnabled); - pw.println(" mSoundNotificationKey=" + mSoundNotificationKey); - pw.println(" mVibrateNotificationKey=" + mVibrateNotificationKey); - pw.println(" mDisableNotificationEffects=" + mDisableNotificationEffects); - pw.println(" mCallState=" + callStateToString(mCallState)); - pw.println(" mSystemReady=" + mSystemReady); pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate); pw.println(" hideSilentStatusBar=" + mPreferencesHelper.shouldHideSilentStatusIcons()); - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.dump(pw, " ", filter); - } + mAttentionHelper.dump(pw, " ", filter); } pw.println(" mArchive=" + mArchive.toString()); mArchive.dumpImpl(pw, filter); @@ -8379,11 +8084,7 @@ public class NotificationManagerService extends SystemService { boolean wasPosted = removeFromNotificationListsLocked(r); cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null, SystemClock.elapsedRealtime()); - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.updateLightsLocked(); - } else { - updateLightsLocked(); - } + mAttentionHelper.updateLightsLocked(); if (isSnoozable(r)) { if (mSnoozeCriterionId != null) { mAssistants.notifyAssistantSnoozedLocked(r, mSnoozeCriterionId); @@ -8519,11 +8220,7 @@ public class NotificationManagerService extends SystemService { cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, mSendDelete, childrenFlagChecker, mReason, mCancellationElapsedTimeMs); - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.updateLightsLocked(); - } else { - updateLightsLocked(); - } + mAttentionHelper.updateLightsLocked(); if (mShortcutHelper != null) { mShortcutHelper.maybeListenForShortcutChangesForBubbles(r, true /* isRemoved */, @@ -8802,6 +8499,11 @@ public class NotificationManagerService extends SystemService { r.isUpdate = true; final boolean isInterruptive = isVisuallyInterruptive(old, r); r.setTextChanged(isInterruptive); + if (updateRankingTime()) { + if (isInterruptive) { + r.resetRankingTime(); + } + } } mNotificationsByKey.put(n.getKey(), r); @@ -8818,14 +8520,10 @@ public class NotificationManagerService extends SystemService { int buzzBeepBlinkLoggingCode = 0; if (!r.isHidden()) { - if (Flags.refactorAttentionHelper()) { - buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r, + buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals( - mUserProfiles.isCurrentProfile(r.getUserId()), - mListenerHints)); - } else { - buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r); - } + mUserProfiles.isCurrentProfile(r.getUserId()), + mListenerHints)); } if (notification.getSmallIcon() != null) { @@ -9150,425 +8848,6 @@ public class NotificationManagerService extends SystemService { } } - @VisibleForTesting - @GuardedBy("mNotificationLock") - /** - * Determine whether this notification should attempt to make noise, vibrate, or flash the LED - * @return buzzBeepBlink - bitfield (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0) - */ - int buzzBeepBlinkLocked(NotificationRecord record) { - if (mIsAutomotive && !mNotificationEffectsEnabledForAutomotive) { - return 0; - } - boolean buzz = false; - boolean beep = false; - boolean blink = false; - - final String key = record.getKey(); - - // Should this notification make noise, vibe, or use the LED? - final boolean aboveThreshold = - mIsAutomotive - ? record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT - : record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT; - // Remember if this notification already owns the notification channels. - boolean wasBeep = key != null && key.equals(mSoundNotificationKey); - boolean wasBuzz = key != null && key.equals(mVibrateNotificationKey); - // These are set inside the conditional if the notification is allowed to make noise. - boolean hasValidVibrate = false; - boolean hasValidSound = false; - boolean sentAccessibilityEvent = false; - - // If the notification will appear in the status bar, it should send an accessibility event - final boolean suppressedByDnd = record.isIntercepted() - && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0; - if (!record.isUpdate - && record.getImportance() > IMPORTANCE_MIN - && !suppressedByDnd - && isNotificationForCurrentUser(record)) { - sendAccessibilityEvent(record); - sentAccessibilityEvent = true; - } - - if (aboveThreshold && isNotificationForCurrentUser(record)) { - if (mSystemReady && mAudioManager != null) { - Uri soundUri = record.getSound(); - hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri); - VibrationEffect vibration = record.getVibration(); - // Demote sound to vibration if vibration missing & phone in vibration mode. - if (vibration == null - && hasValidSound - && (mAudioManager.getRingerModeInternal() - == AudioManager.RINGER_MODE_VIBRATE) - && mAudioManager.getStreamVolume( - AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) == 0) { - boolean insistent = (record.getFlags() & Notification.FLAG_INSISTENT) != 0; - vibration = mVibratorHelper.createFallbackVibration(insistent); - } - hasValidVibrate = vibration != null; - boolean hasAudibleAlert = hasValidSound || hasValidVibrate; - if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) { - if (!sentAccessibilityEvent) { - sendAccessibilityEvent(record); - sentAccessibilityEvent = true; - } - if (DBG) Slog.v(TAG, "Interrupting!"); - boolean isInsistentUpdate = isInsistentUpdate(record); - if (hasValidSound) { - if (isInsistentUpdate) { - // don't reset insistent sound, it's jarring - beep = true; - } else { - if (isInCall()) { - playInCallNotification(); - beep = true; - } else { - beep = playSound(record, soundUri); - } - if (beep) { - mSoundNotificationKey = key; - } - } - } - - final boolean ringerModeSilent = - mAudioManager.getRingerModeInternal() - == AudioManager.RINGER_MODE_SILENT; - if (!isInCall() && hasValidVibrate && !ringerModeSilent) { - if (isInsistentUpdate) { - buzz = true; - } else { - buzz = playVibration(record, vibration, hasValidSound); - if (buzz) { - mVibrateNotificationKey = key; - } - } - } - - // Try to start flash notification event whenever an audible and non-suppressed - // notification is received - mAccessibilityManager.startFlashNotificationEvent(getContext(), - AccessibilityManager.FLASH_REASON_NOTIFICATION, - record.getSbn().getPackageName()); - - } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) { - hasValidSound = false; - } - } - } - // If a notification is updated to remove the actively playing sound or vibrate, - // cancel that feedback now - if (wasBeep && !hasValidSound) { - clearSoundLocked(); - } - if (wasBuzz && !hasValidVibrate) { - clearVibrateLocked(); - } - - // light - // release the light - boolean wasShowLights = mLights.remove(key); - if (canShowLightsLocked(record, aboveThreshold)) { - mLights.add(key); - updateLightsLocked(); - if (mUseAttentionLight && mAttentionLight != null) { - mAttentionLight.pulse(); - } - blink = true; - } else if (wasShowLights) { - updateLightsLocked(); - } - final int buzzBeepBlink = (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0); - if (buzzBeepBlink > 0) { - // Ignore summary updates because we don't display most of the information. - if (record.getSbn().isGroup() && record.getSbn().getNotification().isGroupSummary()) { - if (DEBUG_INTERRUPTIVENESS) { - Slog.v(TAG, "INTERRUPTIVENESS: " - + record.getKey() + " is not interruptive: summary"); - } - } else if (record.canBubble()) { - if (DEBUG_INTERRUPTIVENESS) { - Slog.v(TAG, "INTERRUPTIVENESS: " - + record.getKey() + " is not interruptive: bubble"); - } - } else { - record.setInterruptive(true); - if (DEBUG_INTERRUPTIVENESS) { - Slog.v(TAG, "INTERRUPTIVENESS: " - + record.getKey() + " is interruptive: alerted"); - } - } - MetricsLogger.action(record.getLogMaker() - .setCategory(MetricsEvent.NOTIFICATION_ALERT) - .setType(MetricsEvent.TYPE_OPEN) - .setSubtype(buzzBeepBlink)); - EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, 0); - } - record.setAudiblyAlerted(buzz || beep); - return buzzBeepBlink; - } - - @GuardedBy("mNotificationLock") - boolean canShowLightsLocked(final NotificationRecord record, boolean aboveThreshold) { - // device lacks light - if (!mHasLight) { - return false; - } - // user turned lights off globally - if (!mNotificationPulseEnabled) { - return false; - } - // the notification/channel has no light - if (record.getLight() == null) { - return false; - } - // unimportant notification - if (!aboveThreshold) { - return false; - } - // suppressed due to DND - if ((record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_LIGHTS) != 0) { - return false; - } - // Suppressed because it's a silent update - final Notification notification = record.getNotification(); - if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) { - return false; - } - // Suppressed because another notification in its group handles alerting - if (record.getSbn().isGroup() && record.getNotification().suppressAlertingDueToGrouping()) { - return false; - } - // not if in call - if (isInCall()) { - return false; - } - // check current user - if (!isNotificationForCurrentUser(record)) { - return false; - } - // Light, but only when the screen is off - return true; - } - - @GuardedBy("mNotificationLock") - boolean isInsistentUpdate(final NotificationRecord record) { - return (Objects.equals(record.getKey(), mSoundNotificationKey) - || Objects.equals(record.getKey(), mVibrateNotificationKey)) - && isCurrentlyInsistent(); - } - - @GuardedBy("mNotificationLock") - boolean isCurrentlyInsistent() { - return isLoopingRingtoneNotification(mNotificationsByKey.get(mSoundNotificationKey)) - || isLoopingRingtoneNotification(mNotificationsByKey.get(mVibrateNotificationKey)); - } - - @GuardedBy("mNotificationLock") - boolean shouldMuteNotificationLocked(final NotificationRecord record) { - // Suppressed because it's a silent update - final Notification notification = record.getNotification(); - if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) { - return true; - } - - // Suppressed because a user manually unsnoozed something (or similar) - if (record.shouldPostSilently()) { - return true; - } - - // muted by listener - final String disableEffects = disableNotificationEffects(record); - if (disableEffects != null) { - ZenLog.traceDisableEffects(record, disableEffects); - return true; - } - - // suppressed due to DND - if (record.isIntercepted()) { - return true; - } - - // Suppressed because another notification in its group handles alerting - if (record.getSbn().isGroup()) { - if (notification.suppressAlertingDueToGrouping()) { - return true; - } - } - - // Suppressed for being too recently noisy - final String pkg = record.getSbn().getPackageName(); - if (mUsageStats.isAlertRateLimited(pkg)) { - Slog.e(TAG, "Muting recently noisy " + record.getKey()); - return true; - } - - // A different looping ringtone, such as an incoming call is playing - if (isCurrentlyInsistent() && !isInsistentUpdate(record)) { - return true; - } - - // Suppressed since it's a non-interruptive update to a bubble-suppressed notification - final boolean isBubbleOrOverflowed = record.canBubble() && (record.isFlagBubbleRemoved() - || record.getNotification().isBubbleNotification()); - if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed - && record.getNotification().getBubbleMetadata() != null) { - if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) { - return true; - } - } - - return false; - } - - @GuardedBy("mNotificationLock") - private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) { - if (playingRecord != null) { - if (playingRecord.getAudioAttributes().getUsage() == USAGE_NOTIFICATION_RINGTONE - && (playingRecord.getNotification().flags & FLAG_INSISTENT) != 0) { - return true; - } - } - return false; - } - - private boolean playSound(final NotificationRecord record, Uri soundUri) { - final boolean shouldPlay; - if (focusExclusiveWithRecording()) { - // flagged path - shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes()); - } else { - // legacy path - // play notifications if there is no user of exclusive audio focus - // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or - // VIBRATE ringer mode) - shouldPlay = !mAudioManager.isAudioFocusExclusive() - && (mAudioManager.getStreamVolume( - AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0); - } - if (!shouldPlay) { - if (DBG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume"); - return false; - } - - boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0; - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - if (DBG) { - Slog.v(TAG, "Playing sound " + soundUri - + " with attributes " + record.getAudioAttributes()); - } - player.playAsync(soundUri, record.getSbn().getUser(), looping, - record.getAudioAttributes(), 1.0f); - return true; - } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); - } - return false; - } - - private boolean playVibration(final NotificationRecord record, final VibrationEffect effect, - boolean delayVibForSound) { - // Escalate privileges so we can use the vibrator even if the - // notifying app does not have the VIBRATE permission. - final long identity = Binder.clearCallingIdentity(); - try { - if (delayVibForSound) { - new Thread(() -> { - // delay the vibration by the same amount as the notification sound - final int waitMs = mAudioManager.getFocusRampTimeMs( - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, - record.getAudioAttributes()); - if (DBG) { - Slog.v(TAG, "Delaying vibration for notification " - + record.getKey() + " by " + waitMs + "ms"); - } - try { - Thread.sleep(waitMs); - } catch (InterruptedException e) { } - // Notifications might be canceled before it actually vibrates due to waitMs, - // so need to check that the notification is still valid for vibrate. - synchronized (mNotificationLock) { - if (mNotificationsByKey.get(record.getKey()) != null) { - if (record.getKey().equals(mVibrateNotificationKey)) { - vibrate(record, effect, true); - } else { - if (DBG) { - Slog.v(TAG, "No vibration for notification " - + record.getKey() + ": a new notification is " - + "vibrating, or effects were cleared while waiting"); - } - } - } else { - Slog.w(TAG, "No vibration for canceled notification " - + record.getKey()); - } - } - }).start(); - } else { - vibrate(record, effect, false); - } - return true; - } finally{ - Binder.restoreCallingIdentity(identity); - } - } - - private void vibrate(NotificationRecord record, VibrationEffect effect, boolean delayed) { - // We need to vibrate as "android" so we can breakthrough DND. VibratorManagerService - // doesn't have a concept of vibrating on an app's behalf, so add the app information - // to the reason so we can still debug from bugreports - String reason = "Notification (" + record.getSbn().getOpPkg() + " " - + record.getSbn().getUid() + ") " + (delayed ? "(Delayed)" : ""); - mVibratorHelper.vibrate(effect, record.getAudioAttributes(), reason); - } - - private boolean isNotificationForCurrentUser(NotificationRecord record) { - final int currentUser; - final long token = Binder.clearCallingIdentity(); - try { - currentUser = ActivityManager.getCurrentUser(); - } finally { - Binder.restoreCallingIdentity(token); - } - return (record.getUserId() == UserHandle.USER_ALL || - record.getUserId() == currentUser || - mUserProfiles.isCurrentProfile(record.getUserId())); - } - - protected void playInCallNotification() { - final ContentResolver cr = getContext().getContentResolver(); - if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL - && Settings.Secure.getIntForUser(cr, - Settings.Secure.IN_CALL_NOTIFICATION_ENABLED, 1, cr.getUserId()) != 0) { - new Thread() { - @Override - public void run() { - final long identity = Binder.clearCallingIdentity(); - try { - final IRingtonePlayer player = mAudioManager.getRingtonePlayer(); - if (player != null) { - if (mCallNotificationToken != null) { - player.stop(mCallNotificationToken); - } - mCallNotificationToken = new Binder(); - player.play(mCallNotificationToken, mInCallNotificationUri, - mInCallNotificationAudioAttributes, - mInCallNotificationVolume, false); - } - } catch (RemoteException e) { - } finally { - Binder.restoreCallingIdentity(identity); - } - } - }.start(); - } - } - @GuardedBy("mToastQueue") void showNextToastLocked(boolean lastToastWasTextRecord) { if (mIsCurrentToastShown) { @@ -9840,13 +9119,10 @@ public class NotificationManagerService extends SystemService { || interruptiveChanged; if (interceptBefore && !record.isIntercepted() && record.isNewEnoughForAlerting(System.currentTimeMillis())) { - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.buzzBeepBlinkLocked(record, - new NotificationAttentionHelper.Signals( - mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints)); - } else { - buzzBeepBlinkLocked(record); - } + + mAttentionHelper.buzzBeepBlinkLocked(record, + new NotificationAttentionHelper.Signals(mUserProfiles.isCurrentProfile( + record.getUserId()), mListenerHints)); // Log alert after change in intercepted state to Zen Log as well ZenLog.traceAlertOnUpdatedIntercept(record); @@ -10113,37 +9389,6 @@ public class NotificationManagerService extends SystemService { return (x < low) ? low : ((x > high) ? high : x); } - void sendAccessibilityEvent(NotificationRecord record) { - if (!mAccessibilityManager.isEnabled()) { - return; - } - - final Notification notification = record.getNotification(); - final CharSequence packageName = record.getSbn().getPackageName(); - final AccessibilityEvent event = - AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); - event.setPackageName(packageName); - event.setClassName(Notification.class.getName()); - final int visibilityOverride = record.getPackageVisibilityOverride(); - final int notifVisibility = visibilityOverride == NotificationManager.VISIBILITY_NO_OVERRIDE - ? notification.visibility : visibilityOverride; - final int userId = record.getUser().getIdentifier(); - final boolean needPublic = userId >= 0 && mKeyguardManager.isDeviceLocked(userId); - if (needPublic && notifVisibility != Notification.VISIBILITY_PUBLIC) { - // Emit the public version if we're on the lockscreen and this notification isn't - // publicly visible. - event.setParcelableData(notification.publicVersion); - } else { - event.setParcelableData(notification); - } - final CharSequence tickerText = notification.tickerText; - if (!TextUtils.isEmpty(tickerText)) { - event.getText().add(tickerText); - } - - mAccessibilityManager.sendAccessibilityEvent(event); - } - /** * Removes all NotificationsRecords with the same key as the given notification record * from both lists. Do not call this method while iterating over either list. @@ -10228,22 +9473,7 @@ public class NotificationManagerService extends SystemService { } } - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.clearEffectsLocked(canceledKey); - } else { - // sound - if (canceledKey.equals(mSoundNotificationKey)) { - clearSoundLocked(); - } - - // vibrate - if (canceledKey.equals(mVibrateNotificationKey)) { - clearVibrateLocked(); - } - - // light - mLights.remove(canceledKey); - } + mAttentionHelper.clearEffectsLocked(canceledKey); } // Record usage stats @@ -10592,11 +9822,7 @@ public class NotificationManagerService extends SystemService { cancellationElapsedTimeMs); } } - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.updateLightsLocked(); - } else { - updateLightsLocked(); - } + mAttentionHelper.updateLightsLocked(); } } @@ -10745,37 +9971,6 @@ public class NotificationManagerService extends SystemService { } @GuardedBy("mNotificationLock") - void updateLightsLocked() - { - if (mNotificationLight == null) { - return; - } - - // handle notification lights - NotificationRecord ledNotification = null; - while (ledNotification == null && !mLights.isEmpty()) { - final String owner = mLights.get(mLights.size() - 1); - ledNotification = mNotificationsByKey.get(owner); - if (ledNotification == null) { - Slog.wtfStack(TAG, "LED Notification does not exist: " + owner); - mLights.remove(owner); - } - } - - // Don't flash while we are in a call or screen is on - if (ledNotification == null || isInCall() || mScreenOn) { - mNotificationLight.turnOff(); - } else { - NotificationRecord.Light light = ledNotification.getLight(); - if (light != null && mNotificationPulseEnabled) { - // pulse repeatedly - mNotificationLight.setFlashing(light.color, LogicalLight.LIGHT_FLASH_TIMED, - light.onMs, light.offMs); - } - } - } - - @GuardedBy("mNotificationLock") @NonNull List<NotificationRecord> findCurrentAndSnoozedGroupNotificationsLocked(String pkg, String groupKey, int userId) { @@ -10974,12 +10169,6 @@ public class NotificationManagerService extends SystemService { } } - private void updateNotificationPulse() { - synchronized (mNotificationLock) { - updateLightsLocked(); - } - } - protected boolean isCallingUidSystem() { final int uid = Binder.getCallingUid(); return uid == Process.SYSTEM_UID; @@ -11350,18 +10539,6 @@ public class NotificationManagerService extends SystemService { } } - private boolean isInCall() { - if (mInCallStateOffHook) { - return true; - } - int audioMode = mAudioManager.getMode(); - if (audioMode == AudioManager.MODE_IN_CALL - || audioMode == AudioManager.MODE_IN_COMMUNICATION) { - return true; - } - return false; - } - public class NotificationAssistants extends ManagedServices { static final String TAG_ENABLED_NOTIFICATION_ASSISTANTS = "enabled_assistants"; diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 6ab4b994df73..7e58d0af6195 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -15,6 +15,7 @@ */ package com.android.server.notification; +import static android.app.Flags.updateRankingTime; import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; @@ -65,14 +66,12 @@ import android.util.Log; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import android.widget.RemoteViews; - import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.uri.UriGrantsManagerInternal; - import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; @@ -1090,8 +1089,14 @@ public final class NotificationRecord { private long calculateRankingTimeMs(long previousRankingTimeMs) { Notification n = getNotification(); // Take developer provided 'when', unless it's in the future. - if (n.when != 0 && n.when <= getSbn().getPostTime()) { - return n.when; + if (updateRankingTime()) { + if (n.when != n.creationTime && n.when <= getSbn().getPostTime()){ + return n.when; + } + } else { + if (n.when != 0 && n.when <= getSbn().getPostTime()) { + return n.when; + } } // If we've ranked a previous instance with a timestamp, inherit it. This case is // important in order to have ranking stability for updating notifications. @@ -1193,6 +1198,12 @@ public final class NotificationRecord { return mPeopleOverride; } + public void resetRankingTime() { + if (updateRankingTime()) { + mRankingTimeMs = calculateRankingTimeMs(getSbn().getPostTime()); + } + } + public void setInterruptive(boolean interruptive) { mIsInterruptive = interruptive; final long now = System.currentTimeMillis(); diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 596a0b0fd42c..4c95e8305c17 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -162,9 +162,8 @@ import java.util.zip.ZipOutputStream; public class LauncherAppsService extends SystemService { private static final String WM_TRACE_DIR = "/data/misc/wmtrace/"; private static final String VC_FILE_SUFFIX = ".vc"; - // TODO(b/310027945): Update the intent name. private static final String PS_SETTINGS_INTENT = - "com.android.settings.action.PRIVATE_SPACE_SETUP_FLOW"; + "com.android.settings.action.OPEN_PRIVATE_SPACE_SETTINGS"; private static final Set<PosixFilePermission> WM_TRACE_FILE_PERMISSIONS = Set.of( PosixFilePermission.OWNER_WRITE, @@ -1801,15 +1800,26 @@ public class LauncherAppsService extends SystemService { Slog.e(TAG, "Caller cannot access hidden profiles"); return null; } + final int callingUser = getCallingUserId(); + final int callingUid = getCallingUid(); final long identity = Binder.clearCallingIdentity(); try { Intent psSettingsIntent = new Intent(PS_SETTINGS_INTENT); psSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - final PendingIntent pi = PendingIntent.getActivity(mContext, + List<ResolveInfo> ri = mPackageManagerInternal.queryIntentActivities( + psSettingsIntent, + psSettingsIntent.resolveTypeIfNeeded(mContext.getContentResolver()), + PackageManager.MATCH_SYSTEM_ONLY, callingUid, callingUser); + if (ri.isEmpty()) { + return null; + } + final PendingIntent pi = PendingIntent.getActivityAsUser(mContext, /* requestCode */ 0, psSettingsIntent, - PendingIntent.FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT); + PendingIntent.FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT, + null, + UserHandle.of(callingUser)); return pi == null ? null : pi.getIntentSender(); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index ec98fff25af7..e2f4d18bbd6d 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -293,9 +293,26 @@ public class PackageArchiver { return START_PERMISSION_DENIED; } - Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); - try { + boolean openAppDetailsIfOngoingUnarchival = getAppOpsManager().checkOp( + AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName) + == MODE_ALLOWED; + if (openAppDetailsIfOngoingUnarchival) { + PackageInstaller.SessionInfo activeUnarchivalSession = getActiveUnarchivalSession( + packageName, userId); + if (activeUnarchivalSession != null) { + mPm.mHandler.post(() -> { + Slog.i(TAG, "Opening app details page for ongoing unarchival of: " + + packageName); + getLauncherApps().startPackageInstallerSessionDetailsActivity( + activeUnarchivalSession, null, null); + }); + return START_ABORTED; + } + } + + Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); + requestUnarchive(packageName, callerPackageName, getOrCreateLauncherListener(userId, packageName), UserHandle.of(userId), @@ -793,8 +810,27 @@ public class PackageArchiver { } } - mPm.mHandler.post( - () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId)); + mPm.mHandler.post(() -> { + Slog.i(TAG, "Starting app unarchival for: " + packageName); + unarchiveInternal(packageName, userHandle, installerPackage, + draftSessionId); + }); + } + + @Nullable + private PackageInstaller.SessionInfo getActiveUnarchivalSession(String packageName, + int userId) { + List<PackageInstaller.SessionInfo> activeSessions = + mPm.mInstallerService.getAllSessions(userId).getList(); + for (int idx = 0; idx < activeSessions.size(); idx++) { + PackageInstaller.SessionInfo activeSession = activeSessions.get(idx); + if (activeSession.appPackageName.equals(packageName) + && activeSession.userId == userId && activeSession.active + && activeSession.isUnarchival()) { + return activeSession; + } + } + return null; } private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver, diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 621026751b1b..095a233bde64 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6261,9 +6261,22 @@ public class PackageManagerService implements PackageSender, TestUtilityService packageStateWrite.setMimeGroup(mimeGroup, mimeTypesSet); }); if (mComponentResolver.updateMimeGroup(snapshotComputer(), packageName, mimeGroup)) { - Binder.withCleanCallingIdentity(() -> - mPreferredActivityHelper.clearPackagePreferredActivities(packageName, - UserHandle.USER_ALL)); + Binder.withCleanCallingIdentity(() -> { + mPreferredActivityHelper.clearPackagePreferredActivities(packageName, + UserHandle.USER_ALL); + // Send the ACTION_PACKAGE_CHANGED when the mimeGroup has changes + final Computer snapShot = snapshotComputer(); + final ArrayList<String> components = new ArrayList<>( + Collections.singletonList(packageName)); + final int appId = packageState.getAppId(); + final int[] userIds = resolveUserIds(UserHandle.USER_ALL); + final String reason = "The mimeGroup is changed"; + for (int i = 0; i < userIds.length; i++) { + final int packageUid = UserHandle.getUid(userIds[i], appId); + mBroadcastHelper.sendPackageChangedBroadcast(snapShot, packageName, + true /* dontKillApp */, components, packageUid, reason); + } + }); } scheduleWriteSettings(); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 88e75966b12e..f5ac8306cfa9 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1717,20 +1717,26 @@ public class UserManagerService extends IUserManager.Stub { return false; } - if (android.multiuser.Flags.showSetScreenLockDialog()) { - // Show the prompt to set a new screen lock if the device does not have one - final KeyguardManager km = mContext.getSystemService(KeyguardManager.class); - if (km != null && !km.isDeviceSecure()) { - Intent setScreenLockPromptIntent = - SetScreenLockDialogActivity - .createBaseIntent(LAUNCH_REASON_DISABLE_QUIET_MODE); - setScreenLockPromptIntent.putExtra(EXTRA_ORIGIN_USER_ID, userId); - mContext.startActivity(setScreenLockPromptIntent); - return false; - } + final KeyguardManager km = mContext.getSystemService(KeyguardManager.class); + if (km != null && km.isDeviceSecure()) { + showConfirmCredentialToDisableQuietMode(userId, target, callingPackage); + return false; + } else if (km != null && !km.isDeviceSecure() + && android.multiuser.Flags.showSetScreenLockDialog() + // TODO(b/330720545): Add a better way to accomplish this, also use it + // to block profile creation w/o device credentials present. + && Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0, userId) == 1) { + Intent setScreenLockPromptIntent = + SetScreenLockDialogActivity + .createBaseIntent(LAUNCH_REASON_DISABLE_QUIET_MODE); + setScreenLockPromptIntent.putExtra(EXTRA_ORIGIN_USER_ID, userId); + mContext.startActivity(setScreenLockPromptIntent); + return false; + } else { + Slog.w(LOG_TAG, "Allowing profile unlock even when device credentials " + + "are not set for user " + userId); } - showConfirmCredentialToDisableQuietMode(userId, target, callingPackage); - return false; } } final boolean hasUnifiedChallenge = diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index b18503d7d5cb..1c70af0a56ea 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -596,7 +596,7 @@ public class PackageInfoUtils { ai.requiredDisplayCategory = a.getRequiredDisplayCategory(); ai.requireContentUriPermissionFromCaller = a.getRequireContentUriPermissionFromCaller(); ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts()); - assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId); return ai; } @@ -659,7 +659,7 @@ public class PackageInfoUtils { // Backwards compatibility, coerce to null if empty si.metaData = metaData.isEmpty() ? null : metaData; } - assignFieldsComponentInfoParsedMainComponent(si, s, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(si, s, pkgSetting, userId); return si; } @@ -710,7 +710,7 @@ public class PackageInfoUtils { pi.metaData = metaData.isEmpty() ? null : metaData; } pi.applicationInfo = applicationInfo; - assignFieldsComponentInfoParsedMainComponent(pi, p, pkgSetting, state, userId); + assignFieldsComponentInfoParsedMainComponent(pi, p, pkgSetting, userId); return pi; } @@ -903,13 +903,8 @@ public class PackageInfoUtils { private static void assignFieldsComponentInfoParsedMainComponent( @NonNull ComponentInfo info, @NonNull ParsedMainComponent component, - @NonNull PackageStateInternal pkgSetting, @NonNull PackageUserStateInternal state, - @UserIdInt int userId) { + @NonNull PackageStateInternal pkgSetting, @UserIdInt int userId) { assignFieldsComponentInfoParsedMainComponent(info, component); - // overwrite the enabled state with the current user state - info.enabled = PackageUserStateUtils.isEnabled(state, info.applicationInfo.enabled, - info.enabled, info.name, /* flags */ 0); - Pair<CharSequence, Integer> labelAndIcon = ParsedComponentStateUtils.getNonLocalizedLabelAndIcon(component, pkgSetting, userId); diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 3f041cb48ee2..1da9f25afbc7 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1723,7 +1723,7 @@ final class AccessibilityController { mA11yWindowsPopulator.populateVisibleWindowsOnScreenLocked( mDisplayId, visibleWindows); - if (!com.android.server.accessibility.Flags.computeWindowChangesOnA11y()) { + if (!com.android.server.accessibility.Flags.computeWindowChangesOnA11yV2()) { windows = buildWindowInfoListLocked(visibleWindows, screenSize); } @@ -1732,7 +1732,7 @@ final class AccessibilityController { topFocusedWindowToken = topFocusedWindowState.mClient.asBinder(); } - if (com.android.server.accessibility.Flags.computeWindowChangesOnA11y()) { + if (com.android.server.accessibility.Flags.computeWindowChangesOnA11yV2()) { mCallback.onAccessibilityWindowsChanged(forceSend, topFocusedDisplayId, topFocusedWindowToken, screenSize, visibleWindows); } else { @@ -1747,7 +1747,7 @@ final class AccessibilityController { mInitialized = true; } - // Here are old code paths, called when computeWindowChangesOnA11y flag is disabled. + // Here are old code paths, called when computeWindowChangesOnA11yV2 flag is disabled. // LINT.IfChange /** diff --git a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java index ac3251c9bb12..f6afc52fd8d8 100644 --- a/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java +++ b/services/core/java/com/android/server/wm/AccessibilityWindowsPopulator.java @@ -722,7 +722,7 @@ public final class AccessibilityWindowsPopulator extends WindowInfosListener { } // Compute system bar insets frame if needed. - if (com.android.server.accessibility.Flags.computeWindowChangesOnA11y() + if (com.android.server.accessibility.Flags.computeWindowChangesOnA11yV2() && windowState != null && instance.isUntouchableNavigationBar()) { final InsetsSourceProvider provider = windowState.getControllableInsetProvider(); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 18d2718437a6..9b2ca3953b0d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5786,7 +5786,7 @@ class Task extends TaskFragment { // If we have a watcher, preflight the move before committing to it. First check // for *other* available tasks, but if none are available, then try again allowing the // current task to be selected. - if (isTopRootTaskInDisplayArea() && mAtmService.mController != null) { + if (mAtmService.mController != null && isTopRootTaskInDisplayArea()) { ActivityRecord next = topRunningActivity(null, task.mTaskId); if (next == null) { next = topRunningActivity(null, INVALID_TASK_ID); @@ -5830,6 +5830,15 @@ class Task extends TaskFragment { + tr.mTaskId); if (mTransitionController.isShellTransitionsEnabled()) { + // TODO(b/277838915): Consider to make it concurrent to eliminate the special case. + final Transition collecting = mTransitionController.getCollectingTransition(); + if (collecting != null && collecting.mType == TRANSIT_OPEN) { + // It can be a CLOSING participate of an OPEN transition. This avoids the deferred + // transition from moving task to back after the task was moved to front. + collecting.collect(tr); + moveTaskToBackInner(tr, collecting); + return true; + } final Transition transition = new Transition(TRANSIT_TO_BACK, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); // Guarantee that this gets its own transition by queueing on SyncEngine @@ -5858,7 +5867,7 @@ class Task extends TaskFragment { return true; } - private boolean moveTaskToBackInner(@NonNull Task task, @Nullable Transition transition) { + private void moveTaskToBackInner(@NonNull Task task, @Nullable Transition transition) { final Transition.ReadyCondition movedToBack = new Transition.ReadyCondition("moved-to-back", task); if (transition != null) { @@ -5873,7 +5882,7 @@ class Task extends TaskFragment { if (inPinnedWindowingMode()) { mTaskSupervisor.removeRootTask(this); - return true; + return; } mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */, @@ -5896,7 +5905,6 @@ class Task extends TaskFragment { } else { mRootWindowContainer.resumeFocusedTasksTopActivities(); } - return true; } boolean willActivityBeVisible(IBinder token) { diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index daf8129f1683..7e2ffd486c7e 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -160,7 +160,7 @@ public abstract class WindowManagerInternal { /** * Called when the windows for accessibility changed. This is called if - * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y} is + * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2} is * false. * * @param forceSend Send the windows for accessibility even if they haven't changed. @@ -173,7 +173,7 @@ public abstract class WindowManagerInternal { /** * Called when the windows for accessibility changed. This is called if - * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y} is + * {@link com.android.server.accessibility.Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2} is * true. * TODO(b/322444245): Remove screenSize parameter by getting it from * DisplayManager#getDisplay(int).getRealSize() on the a11y side. diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 1f5451813dae..912ff4ae2022 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -232,6 +232,9 @@ </xs:element> <xs:element name="brightness" type="xs:float" maxOccurs="unbounded"> </xs:element> + <!-- Mapping of current lux to minimum allowed nits values. --> + <xs:element name="luxToMinimumNitsMap" type="nitsMap" maxOccurs="1"> + </xs:element> </xs:sequence> <xs:attribute name="enabled" type="xs:boolean" use="optional"/> </xs:complexType> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index c39c3d7ee7c6..3c708900c64e 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -255,9 +255,11 @@ package com.android.server.display.config { method public java.util.List<java.lang.Float> getBacklight(); method public java.util.List<java.lang.Float> getBrightness(); method public boolean getEnabled(); + method public com.android.server.display.config.NitsMap getLuxToMinimumNitsMap(); method public java.util.List<java.lang.Float> getNits(); method public java.math.BigDecimal getTransitionPoint(); method public void setEnabled(boolean); + method public void setLuxToMinimumNitsMap(com.android.server.display.config.NitsMap); method public void setTransitionPoint(java.math.BigDecimal); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java index d0b46f58d626..a3aa7de00c71 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java @@ -351,6 +351,29 @@ public final class LocaleUtilsTest { assertEquals(1, dest.size()); assertEquals(availableLocales.get(0), dest.get(0)); // "sr-Latn-RS" } + // Locale with deprecated subtag, e.g. CS for Serbia and Montenegro, should not win + // even if the other available locale doesn't have explicit script / country. + // On Android, users don't normally use deprecated subtags unless the application requests. + { + final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-RS"); + final ArrayList<Locale> availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS")); + availableLocales.add(Locale.forLanguageTag("sr-RS")); + final ArrayList<Locale> dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "sr-RS" + } + { + final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-RS"); + final ArrayList<Locale> availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS")); + availableLocales.add(Locale.forLanguageTag("sr")); + final ArrayList<Locale> dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "sr" + } } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 73a2f655da8d..5a022c0f5d27 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -1651,6 +1651,17 @@ public final class DisplayDeviceConfigTest { + " <brightness>0.1</brightness>\n" + " <brightness>0.5</brightness>\n" + " <brightness>1.0</brightness>\n" + + " <luxToMinimumNitsMap>\n" + + " <point>\n" + + " <value>10</value> <nits>0.3</nits>\n" + + " </point>\n" + + " <point>\n" + + " <value>50</value> <nits>0.7</nits>\n" + + " </point>\n" + + " <point>\n" + + " <value>100</value> <nits>1.0</nits>\n" + + " </point>\n" + + " </luxToMinimumNitsMap>\n" + "</lowBrightness>"; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index 5294943fa387..5487bc53ffce 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -35,6 +35,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; @@ -280,7 +281,8 @@ public class BrightnessClamperControllerTest { @Override List<BrightnessStateModifier> getModifiers(DisplayManagerFlags flags, Context context, - Handler handler, BrightnessClamperController.ClamperChangeListener listener) { + Handler handler, BrightnessClamperController.ClamperChangeListener listener, + DisplayDeviceConfig displayDeviceConfig) { return mModifiers; } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt index e4a7d982514f..749c400f819e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt @@ -15,13 +15,18 @@ */ package com.android.server.display.brightness.clamper -import android.os.PowerManager import android.os.UserHandle +import android.platform.test.annotations.RequiresFlagsEnabled import android.provider.Settings import android.testing.TestableContext import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED +import com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED +import com.android.server.display.DisplayDeviceConfig import com.android.server.display.brightness.BrightnessReason +import com.android.server.display.feature.flags.Flags import com.android.server.testutils.TestHandler +import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -32,71 +37,197 @@ private const val userId = UserHandle.USER_CURRENT class BrightnessLowLuxModifierTest { private var mockClamperChangeListener = - mock<BrightnessClamperController.ClamperChangeListener>() + mock<BrightnessClamperController.ClamperChangeListener>() val context = TestableContext( - InstrumentationRegistry.getInstrumentation().getContext()) + InstrumentationRegistry.getInstrumentation().getContext()) private val testHandler = TestHandler(null) private lateinit var modifier: BrightnessLowLuxModifier + private var mockDisplayDeviceConfig = mock<DisplayDeviceConfig>() + + private val LOW_LUX_BRIGHTNESS = 0.1f + private val TRANSITION_POINT = 0.25f + private val NORMAL_RANGE_BRIGHTNESS = 0.3f + @Before fun setUp() { - modifier = BrightnessLowLuxModifier(testHandler, mockClamperChangeListener, context) + modifier = + BrightnessLowLuxModifier(testHandler, + mockClamperChangeListener, + context, + mockDisplayDeviceConfig) + + // values below transition point (even dimmer range) + // nits: 0.1 -> backlight 0.02 -> brightness -> 0.1 + whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 1.0f)) + .thenReturn(0.02f) + whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.02f)) + .thenReturn(LOW_LUX_BRIGHTNESS) + + // values above transition point (noraml range) + // nits: 10 -> backlight 0.2 -> brightness -> 0.3 + whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 2f)) + .thenReturn(0.15f) + whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.15f)) + .thenReturn(0.24f) + + // values above transition point (normal range) + // nits: 10 -> backlight 0.2 -> brightness -> 0.3 + whenever(mockDisplayDeviceConfig.getBacklightFromNits(/* nits= */ 10f)) + .thenReturn(0.2f) + whenever(mockDisplayDeviceConfig.getBrightnessFromBacklight(/* backlight = */ 0.2f)) + .thenReturn(NORMAL_RANGE_BRIGHTNESS) + + // min nits when lux of 400 + whenever(mockDisplayDeviceConfig.getMinNitsFromLux(/* lux= */ 400f)) + .thenReturn(1.0f) + + + whenever(mockDisplayDeviceConfig.lowBrightnessTransitionPoint).thenReturn(TRANSITION_POINT) + testHandler.flush() } @Test - fun testThrottlingBounds() { + fun testSettingOffDisablesModifier() { + // test transition point ensures brightness doesn't drop when setting is off. Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // true - Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId) + Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId) + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) modifier.recalculateLowerBound() testHandler.flush() - assertThat(modifier.isActive).isTrue() + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) + assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off + modifier.onAmbientLuxChange(3000.0f) + testHandler.flush() + assertThat(modifier.isActive).isFalse() + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) + assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off + } - // TODO: code currently returns MIN/MAX; update with lux values - assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testLuxRestrictsBrightnessRange() { + // test that high lux prevents low brightness range. + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.1f, userId) + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) + modifier.onAmbientLuxChange(400.0f) + testHandler.flush() + + assertThat(modifier.isActive).isTrue() + // Test restriction from lux setting + assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) + assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) } @Test - fun testGetReason_UserSet() { + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testUserRestrictsBrightnessRange() { + // test that user minimum nits setting prevents low brightness range. Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId) + Settings.Secure.EVEN_DIMMER_MIN_NITS, 10.0f, userId) + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) modifier.recalculateLowerBound() testHandler.flush() - assertThat(modifier.isActive).isTrue() // Test restriction from user setting + assertThat(modifier.isActive).isTrue() assertThat(modifier.brightnessReason) .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND) + assertThat(modifier.brightnessLowerBound).isEqualTo(NORMAL_RANGE_BRIGHTNESS) } @Test - fun testGetReason_Lux() { + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testOnToOff() { + // test that high lux prevents low brightness range. Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // on Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId) - modifier.onAmbientLuxChange(3000.0f) + Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, userId) + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) + modifier.onAmbientLuxChange(400.0f) testHandler.flush() + assertThat(modifier.isActive).isTrue() + // Test restriction from lux setting + assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) + assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) + + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId) // off + + modifier.recalculateLowerBound() + testHandler.flush() + + assertThat(modifier.isActive).isFalse() + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) + assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testOffToOn() { + // test that high lux prevents low brightness range. + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId) // off + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, userId) + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) + modifier.onAmbientLuxChange(400.0f) + testHandler.flush() + + assertThat(modifier.isActive).isFalse() + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) + assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off + + + + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // on + modifier.recalculateLowerBound() + testHandler.flush() + assertThat(modifier.isActive).isTrue() // Test restriction from lux setting assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) + assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) } @Test - fun testSettingOffDisablesModifier() { + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + fun testDisabledWhenAutobrightnessIsOff() { + // test that high lux prevents low brightness range. Settings.Secure.putIntForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId) - assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) - modifier.onAmbientLuxChange(3000.0f) + Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) // on + Settings.Secure.putFloatForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_MIN_NITS, 1.0f, userId) + + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED) + modifier.onAmbientLuxChange(400.0f) testHandler.flush() + + assertThat(modifier.isActive).isTrue() + // Test restriction from lux setting + assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) + assertThat(modifier.brightnessLowerBound).isEqualTo(LOW_LUX_BRIGHTNESS) + + + modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_DISABLED) + modifier.onAmbientLuxChange(400.0f) + testHandler.flush() + assertThat(modifier.isActive).isFalse() - assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) + // Test restriction from lux setting + assertThat(modifier.brightnessReason).isEqualTo(0) + assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT) } } + diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java index 680ab1634cb2..daa827eacf44 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -186,7 +186,7 @@ public final class ServiceBindingOomAdjPolicyTest { doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer(); doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncAtEarliestLSP( any()); - doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(), + doNothing().when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(), anyInt(), anyLong()); mCurrentCallingUid = TEST_APP1_UID; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java index 8717a0500e57..403930d96a12 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java @@ -86,11 +86,12 @@ import java.util.List; // LINT.IfChange /** - * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y enabled. + * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2 + * enabled. * TODO(b/322444245): Merge with AccessibilityWindowManagerWithAccessibilityWindowTest * after completing the flag migration. */ -@RequiresFlagsDisabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y) +@RequiresFlagsDisabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2) public class AccessibilityWindowManagerTest { private static final String PACKAGE_NAME = "com.android.server.accessibility"; private static final boolean FORCE_SEND = true; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java index f44879fa54d9..9083a1e28e2c 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -89,11 +89,11 @@ import java.util.Arrays; import java.util.List; /** - * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y + * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2 * TODO(b/322444245): Merge with AccessibilityWindowManagerTest * after completing the flag migration. */ -@RequiresFlagsEnabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y) +@RequiresFlagsEnabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y_V2) public class AccessibilityWindowManagerWithAccessibilityWindowTest { private static final String PACKAGE_NAME = "com.android.server.accessibility"; private static final boolean FORCE_SEND = true; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java deleted file mode 100644 index 517dcb4f21f6..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ /dev/null @@ -1,1969 +0,0 @@ -/* - * Copyright (C) 2016 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.notification; - -import static android.app.Notification.FLAG_BUBBLE; -import static android.app.Notification.GROUP_ALERT_ALL; -import static android.app.Notification.GROUP_ALERT_CHILDREN; -import static android.app.Notification.GROUP_ALERT_SUMMARY; -import static android.app.NotificationManager.IMPORTANCE_HIGH; -import static android.app.NotificationManager.IMPORTANCE_LOW; -import static android.app.NotificationManager.IMPORTANCE_MIN; -import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; -import static android.media.AudioAttributes.USAGE_NOTIFICATION; -import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.mockito.ArgumentMatchers.anyFloat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.argThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.after; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.KeyguardManager; -import android.app.Notification; -import android.app.Notification.Builder; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.graphics.drawable.Icon; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Handler; -import android.os.Process; -import android.os.RemoteException; -import android.os.UserHandle; -import android.os.VibrationAttributes; -import android.os.VibrationEffect; -import android.os.Vibrator; -import android.provider.Settings; -import android.service.notification.NotificationListenerService; -import android.service.notification.StatusBarNotification; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.view.accessibility.IAccessibilityManager; -import android.view.accessibility.IAccessibilityManagerClient; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.logging.InstanceIdSequence; -import com.android.internal.logging.InstanceIdSequenceFake; -import com.android.internal.util.IntPair; -import com.android.server.UiServiceTestCase; -import com.android.server.lights.LogicalLight; -import com.android.server.pm.PackageManagerService; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.mockito.verification.VerificationMode; - -import java.util.Objects; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service. -public class BuzzBeepBlinkTest extends UiServiceTestCase { - - @Mock AudioManager mAudioManager; - @Mock Vibrator mVibrator; - @Mock android.media.IRingtonePlayer mRingtonePlayer; - @Mock LogicalLight mLight; - @Mock - NotificationManagerService.WorkerHandler mHandler; - @Mock - NotificationUsageStats mUsageStats; - @Mock - IAccessibilityManager mAccessibilityService; - @Mock - KeyguardManager mKeyguardManager; - NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake(); - private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake( - 1 << 30); - - private NotificationManagerService mService; - private String mPkg = "com.android.server.notification"; - private int mId = 1001; - private int mOtherId = 1002; - private String mTag = null; - private int mUid = 1000; - private int mPid = 2000; - private android.os.UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser()); - private NotificationChannel mChannel; - - private VibrateRepeatMatcher mVibrateOnceMatcher = new VibrateRepeatMatcher(-1); - private VibrateRepeatMatcher mVibrateLoopMatcher = new VibrateRepeatMatcher(0); - - private static final long[] CUSTOM_VIBRATION = new long[] { - 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, - 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, - 300, 400, 300, 400, 300, 400, 300, 400, 300, 400, 300, 400 }; - private static final Uri CUSTOM_SOUND = Settings.System.DEFAULT_ALARM_ALERT_URI; - private static final AudioAttributes CUSTOM_ATTRIBUTES = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .build(); - private static final int CUSTOM_LIGHT_COLOR = Color.BLACK; - private static final int CUSTOM_LIGHT_ON = 10000; - private static final int CUSTOM_LIGHT_OFF = 10000; - private static final int MAX_VIBRATION_DELAY = 1000; - private static final float DEFAULT_VOLUME = 1.0f; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - getContext().addMockSystemService(Vibrator.class, mVibrator); - - when(mAudioManager.isAudioFocusExclusive()).thenReturn(false); - when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer); - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10); - // consistent with focus not exclusive and volume not muted - when(mAudioManager.shouldNotificationSoundPlay(any(AudioAttributes.class))) - .thenReturn(true); - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); - when(mAudioManager.getFocusRampTimeMs(anyInt(), any(AudioAttributes.class))).thenReturn(50); - when(mUsageStats.isAlertRateLimited(any())).thenReturn(false); - when(mVibrator.hasFrequencyControl()).thenReturn(false); - when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false); - - long serviceReturnValue = IntPair.of( - AccessibilityManager.STATE_FLAG_ACCESSIBILITY_ENABLED, - AccessibilityEvent.TYPES_ALL_MASK); - when(mAccessibilityService.addClient(any(), anyInt())).thenReturn(serviceReturnValue); - AccessibilityManager accessibilityManager = - new AccessibilityManager(getContext(), Handler.getMain(), mAccessibilityService, - 0, true); - verify(mAccessibilityService).addClient(any(IAccessibilityManagerClient.class), anyInt()); - assertTrue(accessibilityManager.isEnabled()); - - mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger, - mNotificationInstanceIdSequence)); - mService.setVibratorHelper(new VibratorHelper(getContext())); - mService.setAudioManager(mAudioManager); - mService.setSystemReady(true); - mService.setHandler(mHandler); - mService.setLights(mLight); - mService.setScreenOn(false); - mService.setUsageStats(mUsageStats); - mService.setAccessibilityManager(accessibilityManager); - mService.setKeyguardManager(mKeyguardManager); - mService.mScreenOn = false; - mService.mInCallStateOffHook = false; - mService.mNotificationPulseEnabled = true; - - mChannel = new NotificationChannel("test", "test", IMPORTANCE_HIGH); - } - - // - // Convenience functions for creating notification records - // - - private NotificationRecord getNoisyOtherNotification() { - return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - true /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBeepyNotification() { - return getNotificationRecord(mId, false /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBeepyOtherNotification() { - return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBeepyOnceNotification() { - return getNotificationRecord(mId, false /* insistent */, true /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getQuietNotification() { - return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getQuietOtherNotification() { - return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - false /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getQuietOnceNotification() { - return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getInsistentBeepyNotification() { - return getNotificationRecord(mId, true /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getInsistentBeepyOnceNotification() { - return getNotificationRecord(mId, true /* insistent */, true /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getInsistentBeepyLeanbackNotification() { - return getLeanbackNotificationRecord(mId, true /* insistent */, false /* once */, - true /* noisy */, false /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBuzzyNotification() { - return getNotificationRecord(mId, false /* insistent */, false /* once */, - false /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBuzzyOtherNotification() { - return getNotificationRecord(mOtherId, false /* insistent */, false /* once */, - false /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBuzzyOnceNotification() { - return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getInsistentBuzzyNotification() { - return getNotificationRecord(mId, true /* insistent */, false /* once */, - false /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getBuzzyBeepyNotification() { - return getNotificationRecord(mId, false /* insistent */, false /* once */, - true /* noisy */, true /* buzzy*/, false /* lights */); - } - - private NotificationRecord getLightsNotification() { - return getNotificationRecord(mId, false /* insistent */, false /* once */, - false /* noisy */, false /* buzzy*/, true /* lights */); - } - - private NotificationRecord getLightsOnceNotification() { - return getNotificationRecord(mId, false /* insistent */, true /* once */, - false /* noisy */, false /* buzzy*/, true /* lights */); - } - - private NotificationRecord getCallRecord(int id, NotificationChannel channel, boolean looping) { - final Builder builder = new Builder(getContext()) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setPriority(Notification.PRIORITY_HIGH); - Notification n = builder.build(); - if (looping) { - n.flags |= Notification.FLAG_INSISTENT; - } - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid, - mPid, n, mUser, null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, channel); - mService.addNotification(r); - - return r; - } - - private NotificationRecord getNotificationRecord(int id, boolean insistent, boolean once, - boolean noisy, boolean buzzy, boolean lights) { - return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, buzzy, noisy, - lights, null, Notification.GROUP_ALERT_ALL, false); - } - - private NotificationRecord getLeanbackNotificationRecord(int id, boolean insistent, - boolean once, - boolean noisy, boolean buzzy, boolean lights) { - return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, true, true, - true, - null, Notification.GROUP_ALERT_ALL, true); - } - - private NotificationRecord getBeepyNotificationRecord(String groupKey, int groupAlertBehavior) { - return getNotificationRecord(mId, false, false, true, false, false, true, true, true, - groupKey, groupAlertBehavior, false); - } - - private NotificationRecord getLightsNotificationRecord(String groupKey, - int groupAlertBehavior) { - return getNotificationRecord(mId, false, false, false, false, true /*lights*/, true, - true, true, groupKey, groupAlertBehavior, false); - } - - private NotificationRecord getNotificationRecord(int id, - boolean insistent, boolean once, - boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration, - boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior, - boolean isLeanback) { - - final Builder builder = new Builder(getContext()) - .setContentTitle("foo") - .setSmallIcon(android.R.drawable.sym_def_app_icon) - .setPriority(Notification.PRIORITY_HIGH) - .setOnlyAlertOnce(once); - - int defaults = 0; - if (noisy) { - if (defaultSound) { - defaults |= Notification.DEFAULT_SOUND; - mChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, - Notification.AUDIO_ATTRIBUTES_DEFAULT); - } else { - builder.setSound(CUSTOM_SOUND); - mChannel.setSound(CUSTOM_SOUND, CUSTOM_ATTRIBUTES); - } - } else { - mChannel.setSound(null, null); - } - if (buzzy) { - if (defaultVibration) { - defaults |= Notification.DEFAULT_VIBRATE; - } else { - builder.setVibrate(CUSTOM_VIBRATION); - mChannel.setVibrationPattern(CUSTOM_VIBRATION); - } - mChannel.enableVibration(true); - } else { - mChannel.setVibrationPattern(null); - mChannel.enableVibration(false); - } - - if (lights) { - if (defaultLights) { - defaults |= Notification.DEFAULT_LIGHTS; - } else { - builder.setLights(CUSTOM_LIGHT_COLOR, CUSTOM_LIGHT_ON, CUSTOM_LIGHT_OFF); - } - mChannel.enableLights(true); - } else { - mChannel.enableLights(false); - } - builder.setDefaults(defaults); - - builder.setGroup(groupKey); - builder.setGroupAlertBehavior(groupAlertBehavior); - - Notification n = builder.build(); - if (insistent) { - n.flags |= Notification.FLAG_INSISTENT; - } - - Context context = spy(getContext()); - PackageManager packageManager = spy(context.getPackageManager()); - when(context.getPackageManager()).thenReturn(packageManager); - when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) - .thenReturn(isLeanback); - - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, id, mTag, mUid, - mPid, n, mUser, null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(context, sbn, mChannel); - mService.addNotification(r); - return r; - } - - // - // Convenience functions for interacting with mocks - // - - private void verifyNeverBeep() throws RemoteException { - verify(mRingtonePlayer, never()).playAsync(any(), any(), anyBoolean(), any(), anyFloat()); - } - - private void verifyBeepUnlooped() throws RemoteException { - verify(mRingtonePlayer, times(1)).playAsync(any(), any(), eq(false), any(), - eq(DEFAULT_VOLUME)); - } - - private void verifyBeepLooped() throws RemoteException { - verify(mRingtonePlayer, times(1)).playAsync(any(), any(), eq(true), any(), - eq(DEFAULT_VOLUME)); - } - - private void verifyBeep(int times) throws RemoteException { - verify(mRingtonePlayer, times(times)).playAsync(any(), any(), anyBoolean(), any(), - eq(DEFAULT_VOLUME)); - } - - private void verifyNeverStopAudio() throws RemoteException { - verify(mRingtonePlayer, never()).stopAsync(); - } - - private void verifyStopAudio() throws RemoteException { - verify(mRingtonePlayer, times(1)).stopAsync(); - } - - private void verifyNeverVibrate() { - verify(mVibrator, never()).vibrate(anyInt(), anyString(), any(), anyString(), - any(VibrationAttributes.class)); - } - - private void verifyVibrate() { - verifyVibrate(/* times= */ 1); - } - - private void verifyVibrate(int times) { - verifyVibrate(mVibrateOnceMatcher, times(times)); - } - - private void verifyVibrateLooped() { - verifyVibrate(mVibrateLoopMatcher, times(1)); - } - - private void verifyDelayedVibrateLooped() { - verifyVibrate(mVibrateLoopMatcher, timeout(MAX_VIBRATION_DELAY).times(1)); - } - - private void verifyDelayedVibrate(VibrationEffect effect) { - verifyVibrate(argument -> Objects.equals(effect, argument), - timeout(MAX_VIBRATION_DELAY).times(1)); - } - - private void verifyDelayedNeverVibrate() { - verify(mVibrator, after(MAX_VIBRATION_DELAY).never()).vibrate(anyInt(), anyString(), any(), - anyString(), any(VibrationAttributes.class)); - } - - private void verifyVibrate(ArgumentMatcher<VibrationEffect> effectMatcher, - VerificationMode verification) { - ArgumentCaptor<VibrationAttributes> captor = - ArgumentCaptor.forClass(VibrationAttributes.class); - verify(mVibrator, verification).vibrate(eq(Process.SYSTEM_UID), - eq(PackageManagerService.PLATFORM_PACKAGE_NAME), argThat(effectMatcher), - anyString(), captor.capture()); - assertEquals(0, (captor.getValue().getFlags() - & VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)); - } - - private void verifyStopVibrate() { - int alarmClassUsageFilter = - VibrationAttributes.USAGE_CLASS_ALARM | ~VibrationAttributes.USAGE_CLASS_MASK; - verify(mVibrator, times(1)).cancel(eq(alarmClassUsageFilter)); - } - - private void verifyNeverStopVibrate() { - verify(mVibrator, never()).cancel(); - verify(mVibrator, never()).cancel(anyInt()); - } - - private void verifyNeverLights() { - verify(mLight, never()).setFlashing(anyInt(), anyInt(), anyInt(), anyInt()); - } - - private void verifyLights() { - verify(mLight, times(1)).setFlashing(anyInt(), anyInt(), anyInt(), anyInt()); - } - - // - // Tests - // - - @Test - public void testLights() throws Exception { - NotificationRecord r = getLightsNotification(); - r.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); - - mService.buzzBeepBlinkLocked(r); - - verifyLights(); - assertTrue(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testBeep() throws Exception { - NotificationRecord r = getBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepUnlooped(); - verifyNeverVibrate(); - verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt()); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLockedPrivateA11yRedaction() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE); - r.getNotification().visibility = Notification.VISIBILITY_PRIVATE; - when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true); - AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class); - when(accessibilityManager.isEnabled()).thenReturn(true); - mService.setAccessibilityManager(accessibilityManager); - - mService.buzzBeepBlinkLocked(r); - - ArgumentCaptor<AccessibilityEvent> eventCaptor = - ArgumentCaptor.forClass(AccessibilityEvent.class); - - verify(accessibilityManager, times(1)) - .sendAccessibilityEvent(eventCaptor.capture()); - - AccessibilityEvent event = eventCaptor.getValue(); - assertEquals(r.getNotification().publicVersion, event.getParcelableData()); - } - - @Test - public void testLockedOverridePrivateA11yRedaction() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setPackageVisibilityOverride(Notification.VISIBILITY_PRIVATE); - r.getNotification().visibility = Notification.VISIBILITY_PUBLIC; - when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true); - AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class); - when(accessibilityManager.isEnabled()).thenReturn(true); - mService.setAccessibilityManager(accessibilityManager); - - mService.buzzBeepBlinkLocked(r); - - ArgumentCaptor<AccessibilityEvent> eventCaptor = - ArgumentCaptor.forClass(AccessibilityEvent.class); - - verify(accessibilityManager, times(1)) - .sendAccessibilityEvent(eventCaptor.capture()); - - AccessibilityEvent event = eventCaptor.getValue(); - assertEquals(r.getNotification().publicVersion, event.getParcelableData()); - } - - @Test - public void testLockedPublicA11yNoRedaction() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE); - r.getNotification().visibility = Notification.VISIBILITY_PUBLIC; - when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(true); - AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class); - when(accessibilityManager.isEnabled()).thenReturn(true); - mService.setAccessibilityManager(accessibilityManager); - - mService.buzzBeepBlinkLocked(r); - - ArgumentCaptor<AccessibilityEvent> eventCaptor = - ArgumentCaptor.forClass(AccessibilityEvent.class); - - verify(accessibilityManager, times(1)) - .sendAccessibilityEvent(eventCaptor.capture()); - - AccessibilityEvent event = eventCaptor.getValue(); - assertEquals(r.getNotification(), event.getParcelableData()); - } - - @Test - public void testUnlockedPrivateA11yNoRedaction() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setPackageVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE); - r.getNotification().visibility = Notification.VISIBILITY_PRIVATE; - when(mKeyguardManager.isDeviceLocked(anyInt())).thenReturn(false); - AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class); - when(accessibilityManager.isEnabled()).thenReturn(true); - mService.setAccessibilityManager(accessibilityManager); - - mService.buzzBeepBlinkLocked(r); - - ArgumentCaptor<AccessibilityEvent> eventCaptor = - ArgumentCaptor.forClass(AccessibilityEvent.class); - - verify(accessibilityManager, times(1)) - .sendAccessibilityEvent(eventCaptor.capture()); - - AccessibilityEvent event = eventCaptor.getValue(); - assertEquals(r.getNotification(), event.getParcelableData()); - } - - @Test - public void testBeepInsistently() throws Exception { - NotificationRecord r = getInsistentBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepLooped(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoLeanbackBeep() throws Exception { - NotificationRecord r = getInsistentBeepyLeanbackNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoBeepForAutomotiveIfEffectsDisabled() throws Exception { - mService.setIsAutomotive(true); - mService.setNotificationEffectsEnabledForAutomotive(false); - - NotificationRecord r = getBeepyNotification(); - r.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - } - - @Test - public void testNoBeepForImportanceDefaultInAutomotiveIfEffectsEnabled() throws Exception { - mService.setIsAutomotive(true); - mService.setNotificationEffectsEnabledForAutomotive(true); - - NotificationRecord r = getBeepyNotification(); - r.setSystemImportance(NotificationManager.IMPORTANCE_DEFAULT); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - } - - @Test - public void testBeepForImportanceHighInAutomotiveIfEffectsEnabled() throws Exception { - mService.setIsAutomotive(true); - mService.setNotificationEffectsEnabledForAutomotive(true); - - NotificationRecord r = getBeepyNotification(); - r.setSystemImportance(NotificationManager.IMPORTANCE_HIGH); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepUnlooped(); - assertTrue(r.isInterruptive()); - } - - @Test - public void testNoInterruptionForMin() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setSystemImportance(NotificationManager.IMPORTANCE_MIN); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - verifyNeverVibrate(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoInterruptionForIntercepted() throws Exception { - NotificationRecord r = getBeepyNotification(); - r.setIntercepted(true); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - verifyNeverVibrate(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testBeepTwice() throws Exception { - NotificationRecord r = getBeepyNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mRingtonePlayer); - - // update should beep - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - verifyBeepUnlooped(); - verify(mAccessibilityService, times(2)).sendAccessibilityEvent(any(), anyInt()); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testHonorAlertOnlyOnceForBeep() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getBeepyOnceNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mRingtonePlayer); - - // update should not beep - mService.buzzBeepBlinkLocked(s); - verifyNeverBeep(); - verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt()); - } - - @Test - public void testNoisyUpdateDoesNotCancelAudio() throws Exception { - NotificationRecord r = getBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - - verifyNeverStopAudio(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoisyOnceUpdateDoesNotCancelAudio() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getBeepyOnceNotification(); - s.isUpdate = true; - - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(s); - - verifyNeverStopAudio(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - /** - * Tests the case where the user re-posts a {@link Notification} with looping sound where - * {@link Notification.Builder#setOnlyAlertOnce(true)} has been called. This should silence - * the sound associated with the notification. - * @throws Exception - */ - @Test - public void testNoisyOnceUpdateDoesCancelAudio() throws Exception { - NotificationRecord r = getInsistentBeepyNotification(); - NotificationRecord s = getInsistentBeepyOnceNotification(); - s.isUpdate = true; - - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(s); - - verifyStopAudio(); - } - - @Test - public void testQuietUpdateDoesNotCancelAudioFromOther() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getQuietNotification(); - s.isUpdate = true; - NotificationRecord other = getNoisyOtherNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(other); // this takes the audio stream - Mockito.reset(mRingtonePlayer); - - // should not stop noise, since we no longer own it - mService.buzzBeepBlinkLocked(s); // this no longer owns the stream - verifyNeverStopAudio(); - assertTrue(other.isInterruptive()); - assertNotEquals(-1, other.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietInterloperDoesNotCancelAudio() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord other = getQuietOtherNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mRingtonePlayer); - - // should not stop noise, since it does not own it - mService.buzzBeepBlinkLocked(other); - verifyNeverStopAudio(); - } - - @Test - public void testQuietUpdateCancelsAudio() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getQuietNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - Mockito.reset(mRingtonePlayer); - - // quiet update should stop making noise - mService.buzzBeepBlinkLocked(s); - verifyStopAudio(); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietOnceUpdateCancelsAudio() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getQuietOnceNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - Mockito.reset(mRingtonePlayer); - - // stop making noise - this is a weird corner case, but quiet should override once - mService.buzzBeepBlinkLocked(s); - verifyStopAudio(); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testInCallNotification() throws Exception { - NotificationRecord r = getBeepyNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mRingtonePlayer); - - mService.mInCallStateOffHook = true; - mService.buzzBeepBlinkLocked(r); - - verify(mService, times(1)).playInCallNotification(); - verifyNeverBeep(); // doesn't play normal beep - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoDemoteSoundToVibrateIfVibrateGiven() throws Exception { - NotificationRecord r = getBuzzyBeepyNotification(); - assertTrue(r.getSound() != null); - - // the phone is quiet - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); - - mService.buzzBeepBlinkLocked(r); - - verifyDelayedVibrate(r.getVibration()); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoDemoteSoundToVibrateIfNonNotificationStream() throws Exception { - NotificationRecord r = getBeepyNotification(); - assertTrue(r.getSound() != null); - assertNull(r.getVibration()); - - // the phone is quiet - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(1); - // all streams at 1 means no muting from audio framework - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(true); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverVibrate(); - verifyBeepUnlooped(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testDemoteSoundToVibrate() throws Exception { - NotificationRecord r = getBeepyNotification(); - assertTrue(r.getSound() != null); - assertNull(r.getVibration()); - - // the phone is quiet - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); - - mService.buzzBeepBlinkLocked(r); - - verifyDelayedVibrate( - mService.getVibratorHelper().createFallbackVibration(/* insistent= */ false)); - verify(mRingtonePlayer, never()).playAsync - (anyObject(), anyObject(), anyBoolean(), anyObject(), anyFloat()); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testDemoteInsistentSoundToVibrate() throws Exception { - NotificationRecord r = getInsistentBeepyNotification(); - assertTrue(r.getSound() != null); - assertNull(r.getVibration()); - - // the phone is quiet - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); - - mService.buzzBeepBlinkLocked(r); - - verifyDelayedVibrateLooped(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testVibrate() throws Exception { - NotificationRecord r = getBuzzyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - verifyVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testInsistentVibrate() { - NotificationRecord r = getInsistentBuzzyNotification(); - - mService.buzzBeepBlinkLocked(r); - verifyVibrateLooped(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testVibrateTwice() { - NotificationRecord r = getBuzzyNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mVibrator); - - // update should vibrate - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - verifyVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testPostSilently() throws Exception { - NotificationRecord r = getBuzzyNotification(); - r.setPostSilently(true); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummarySilenceChild() throws Exception { - NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); - - mService.buzzBeepBlinkLocked(child); - - verifyNeverBeep(); - assertFalse(child.isInterruptive()); - assertEquals(-1, child.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummaryNoSilenceSummary() throws Exception { - NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - - verifyBeepUnlooped(); - // summaries are never interruptive for notification counts - assertFalse(summary.isInterruptive()); - assertNotEquals(-1, summary.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummaryNoSilenceNonGroupChild() throws Exception { - NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_SUMMARY); - - mService.buzzBeepBlinkLocked(nonGroup); - - verifyBeepUnlooped(); - assertTrue(nonGroup.isInterruptive()); - assertNotEquals(-1, nonGroup.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildSilenceSummary() throws Exception { - NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - - verifyNeverBeep(); - assertFalse(summary.isInterruptive()); - assertEquals(-1, summary.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildNoSilenceChild() throws Exception { - NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); - - mService.buzzBeepBlinkLocked(child); - - verifyBeepUnlooped(); - assertTrue(child.isInterruptive()); - assertNotEquals(-1, child.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildNoSilenceNonGroupSummary() throws Exception { - NotificationRecord nonGroup = getBeepyNotificationRecord(null, GROUP_ALERT_CHILDREN); - - mService.buzzBeepBlinkLocked(nonGroup); - - verifyBeepUnlooped(); - assertTrue(nonGroup.isInterruptive()); - assertNotEquals(-1, nonGroup.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertAllNoSilenceGroup() throws Exception { - NotificationRecord group = getBeepyNotificationRecord("a", GROUP_ALERT_ALL); - - mService.buzzBeepBlinkLocked(group); - - verifyBeepUnlooped(); - assertTrue(group.isInterruptive()); - assertNotEquals(-1, group.getLastAudiblyAlertedMs()); - } - - @Test - public void testHonorAlertOnlyOnceForBuzz() throws Exception { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord s = getBuzzyOnceNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mVibrator); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - - // update should not beep - mService.buzzBeepBlinkLocked(s); - verifyNeverVibrate(); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoisyUpdateDoesNotCancelVibrate() throws Exception { - NotificationRecord r = getBuzzyNotification(); - - mService.buzzBeepBlinkLocked(r); - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - - verifyNeverStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testNoisyOnceUpdateDoesNotCancelVibrate() throws Exception { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord s = getBuzzyOnceNotification(); - s.isUpdate = true; - - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(s); - - verifyNeverStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietUpdateDoesNotCancelVibrateFromOther() throws Exception { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord s = getQuietNotification(); - s.isUpdate = true; - NotificationRecord other = getNoisyOtherNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(other); // this takes the vibrate stream - Mockito.reset(mVibrator); - - // should not stop vibrate, since we no longer own it - mService.buzzBeepBlinkLocked(s); // this no longer owns the stream - verifyNeverStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertTrue(other.isInterruptive()); - assertNotEquals(-1, other.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietInterloperDoesNotCancelVibrate() throws Exception { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord other = getQuietOtherNotification(); - - // set up internal state - mService.buzzBeepBlinkLocked(r); - Mockito.reset(mVibrator); - - // should not stop noise, since it does not own it - mService.buzzBeepBlinkLocked(other); - verifyNeverStopVibrate(); - assertFalse(other.isInterruptive()); - assertEquals(-1, other.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietUpdateCancelsVibrate() { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord s = getQuietNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - verifyVibrate(); - - // quiet update should stop making noise - mService.buzzBeepBlinkLocked(s); - verifyStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietOnceUpdateCancelVibrate() throws Exception { - NotificationRecord r = getBuzzyNotification(); - NotificationRecord s = getQuietOnceNotification(); - s.isUpdate = true; - - // set up internal state - mService.buzzBeepBlinkLocked(r); - verifyVibrate(); - - // stop making noise - this is a weird corner case, but quiet should override once - mService.buzzBeepBlinkLocked(s); - verifyStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testQuietUpdateCancelsDemotedVibrate() throws Exception { - NotificationRecord r = getBeepyNotification(); - NotificationRecord s = getQuietNotification(); - - // the phone is quiet - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); - - mService.buzzBeepBlinkLocked(r); - verifyDelayedVibrate(mService.getVibratorHelper().createFallbackVibration(false)); - - // quiet update should stop making noise - mService.buzzBeepBlinkLocked(s); - verifyStopVibrate(); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - assertFalse(s.isInterruptive()); - assertEquals(-1, s.getLastAudiblyAlertedMs()); - } - - @Test - public void testEmptyUriSoundTreatedAsNoSound() throws Exception { - NotificationChannel channel = new NotificationChannel("test", "test", IMPORTANCE_HIGH); - channel.setSound(Uri.EMPTY, null); - final Notification n = new Builder(getContext(), "test") - .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, - mPid, n, mUser, null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, channel); - mService.addNotification(r); - - mService.buzzBeepBlinkLocked(r); - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testRepeatedSoundOverLimitMuted() throws Exception { - when(mUsageStats.isAlertRateLimited(any())).thenReturn(true); - - NotificationRecord r = getBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testPostingSilentNotificationDoesNotAffectRateLimiting() throws Exception { - NotificationRecord r = getQuietNotification(); - mService.buzzBeepBlinkLocked(r); - - verify(mUsageStats, never()).isAlertRateLimited(any()); - } - - @Test - public void testPostingGroupSuppressedDoesNotAffectRateLimiting() throws Exception { - NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_CHILDREN); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - verify(mUsageStats, never()).isAlertRateLimited(any()); - } - - @Test - public void testGroupSuppressionFailureDoesNotAffectRateLimiting() { - NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_SUMMARY); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - verify(mUsageStats, times(1)).isAlertRateLimited(any()); - } - - @Test - public void testCrossUserSoundMuted() throws Exception { - final Notification n = new Builder(getContext(), "test") - .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - - int userId = mUser.getIdentifier() + 1; - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, - mPid, n, UserHandle.of(userId), null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, - new NotificationChannel("test", "test", IMPORTANCE_HIGH)); - - mService.buzzBeepBlinkLocked(r); - verifyNeverBeep(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testA11yMinInitialPost() throws Exception { - NotificationRecord r = getQuietNotification(); - r.setSystemImportance(IMPORTANCE_MIN); - mService.buzzBeepBlinkLocked(r); - verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt()); - } - - @Test - public void testA11yQuietInitialPost() throws Exception { - NotificationRecord r = getQuietNotification(); - mService.buzzBeepBlinkLocked(r); - verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt()); - } - - @Test - public void testA11yQuietUpdate() throws Exception { - NotificationRecord r = getQuietNotification(); - mService.buzzBeepBlinkLocked(r); - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - verify(mAccessibilityService, times(1)).sendAccessibilityEvent(any(), anyInt()); - } - - @Test - public void testA11yCrossUserEventNotSent() throws Exception { - final Notification n = new Builder(getContext(), "test") - .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - int userId = mUser.getIdentifier() + 1; - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, - mPid, n, UserHandle.of(userId), null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, - new NotificationChannel("test", "test", IMPORTANCE_HIGH)); - - mService.buzzBeepBlinkLocked(r); - - verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt()); - } - - @Test - public void testLightsScreenOn() { - mService.mScreenOn = true; - NotificationRecord r = getLightsNotification(); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertTrue(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsInCall() { - mService.mInCallStateOffHook = true; - NotificationRecord r = getLightsNotification(); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsSilentUpdate() { - NotificationRecord r = getLightsOnceNotification(); - mService.buzzBeepBlinkLocked(r); - verifyLights(); - assertTrue(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - - r = getLightsOnceNotification(); - r.isUpdate = true; - mService.buzzBeepBlinkLocked(r); - // checks that lights happened once, i.e. this new call didn't trigger them again - verifyLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsUnimportant() { - NotificationRecord r = getLightsNotification(); - r.setSystemImportance(IMPORTANCE_LOW); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsNoLights() { - NotificationRecord r = getQuietNotification(); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsNoLightOnDevice() { - mService.mHasLight = false; - NotificationRecord r = getLightsNotification(); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsLightsOffGlobally() { - mService.mNotificationPulseEnabled = false; - NotificationRecord r = getLightsNotification(); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsDndIntercepted() { - NotificationRecord r = getLightsNotification(); - r.setSuppressedVisualEffects(SUPPRESSED_EFFECT_LIGHTS); - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummaryNoLightsChild() { - NotificationRecord child = getLightsNotificationRecord("a", GROUP_ALERT_SUMMARY); - - mService.buzzBeepBlinkLocked(child); - - verifyNeverLights(); - assertFalse(child.isInterruptive()); - assertEquals(-1, child.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummaryLightsSummary() { - NotificationRecord summary = getLightsNotificationRecord("a", GROUP_ALERT_SUMMARY); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - - verifyLights(); - // summaries should never count for interruptiveness counts - assertFalse(summary.isInterruptive()); - assertEquals(-1, summary.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertSummaryLightsNonGroupChild() { - NotificationRecord nonGroup = getLightsNotificationRecord(null, GROUP_ALERT_SUMMARY); - - mService.buzzBeepBlinkLocked(nonGroup); - - verifyLights(); - assertTrue(nonGroup.isInterruptive()); - assertEquals(-1, nonGroup.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildNoLightsSummary() { - NotificationRecord summary = getLightsNotificationRecord("a", GROUP_ALERT_CHILDREN); - summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY; - - mService.buzzBeepBlinkLocked(summary); - - verifyNeverLights(); - assertFalse(summary.isInterruptive()); - assertEquals(-1, summary.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildLightsChild() { - NotificationRecord child = getLightsNotificationRecord("a", GROUP_ALERT_CHILDREN); - - mService.buzzBeepBlinkLocked(child); - - verifyLights(); - assertTrue(child.isInterruptive()); - assertEquals(-1, child.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertChildLightsNonGroupSummary() { - NotificationRecord nonGroup = getLightsNotificationRecord(null, GROUP_ALERT_CHILDREN); - - mService.buzzBeepBlinkLocked(nonGroup); - - verifyLights(); - assertTrue(nonGroup.isInterruptive()); - assertEquals(-1, nonGroup.getLastAudiblyAlertedMs()); - } - - @Test - public void testGroupAlertAllLightsGroup() { - NotificationRecord group = getLightsNotificationRecord("a", GROUP_ALERT_ALL); - - mService.buzzBeepBlinkLocked(group); - - verifyLights(); - assertTrue(group.isInterruptive()); - assertEquals(-1, group.getLastAudiblyAlertedMs()); - } - - @Test - public void testLightsCheckCurrentUser() { - final Notification n = new Builder(getContext(), "test") - .setSmallIcon(android.R.drawable.sym_def_app_icon).build(); - int userId = mUser.getIdentifier() + 10; - StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid, - mPid, n, UserHandle.of(userId), null, System.currentTimeMillis()); - NotificationRecord r = new NotificationRecord(getContext(), sbn, - new NotificationChannel("test", "test", IMPORTANCE_HIGH)); - - mService.buzzBeepBlinkLocked(r); - verifyNeverLights(); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testListenerHintCall() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord r = getCallRecord(1, ringtoneChannel, true); - - mService.setHints(NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - } - - @Test - public void testListenerHintCall_notificationSound() throws Exception { - NotificationRecord r = getBeepyNotification(); - - mService.setHints(NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepUnlooped(); - } - - @Test - public void testListenerHintNotification() throws Exception { - NotificationRecord r = getBeepyNotification(); - - mService.setHints(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - } - - @Test - public void testListenerHintBoth() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord r = getCallRecord(1, ringtoneChannel, true); - NotificationRecord s = getBeepyNotification(); - - mService.setHints(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS - | NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS); - - mService.buzzBeepBlinkLocked(r); - mService.buzzBeepBlinkLocked(s); - - verifyNeverBeep(); - } - - @Test - public void testListenerHintNotification_callSound() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord r = getCallRecord(1, ringtoneChannel, true); - - mService.setHints(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepLooped(); - } - - @Test - public void testCannotInterruptRingtoneInsistentBeep() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - mService.addNotification(ringtoneNotification); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - - NotificationRecord interrupter = getBeepyOtherNotification(); - assertTrue(mService.shouldMuteNotificationLocked(interrupter)); - mService.buzzBeepBlinkLocked(interrupter); - - verifyBeep(1); - - assertFalse(interrupter.isInterruptive()); - assertEquals(-1, interrupter.getLastAudiblyAlertedMs()); - } - - @Test - public void testRingtoneInsistentBeep_canUpdate() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"), - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - mService.addNotification(ringtoneNotification); - assertFalse(mService.shouldMuteNotificationLocked(ringtoneNotification)); - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - verifyDelayedVibrateLooped(); - Mockito.reset(mVibrator); - Mockito.reset(mRingtonePlayer); - - assertFalse(mService.shouldMuteNotificationLocked(ringtoneNotification)); - mService.buzzBeepBlinkLocked(ringtoneNotification); - - // beep wasn't reset - verifyNeverBeep(); - verifyNeverVibrate(); - verifyNeverStopAudio(); - verifyNeverStopVibrate(); - } - - @Test - public void testRingtoneInsistentBeep_clearEffectsStopsSoundAndVibration() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"), - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - mService.addNotification(ringtoneNotification); - assertFalse(mService.shouldMuteNotificationLocked(ringtoneNotification)); - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - verifyDelayedVibrateLooped(); - - mService.clearSoundLocked(); - mService.clearVibrateLocked(); - - verifyStopAudio(); - verifyStopVibrate(); - } - - @Test - public void testRingtoneInsistentBeep_neverVibratesWhenEffectsClearedBeforeDelay() - throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Uri.fromParts("a", "b", "c"), - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - mService.addNotification(ringtoneNotification); - assertFalse(mService.shouldMuteNotificationLocked(ringtoneNotification)); - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - verifyNeverVibrate(); - - mService.clearSoundLocked(); - mService.clearVibrateLocked(); - - verifyStopAudio(); - verifyDelayedNeverVibrate(); - } - - @Test - public void testCannotInterruptRingtoneInsistentBuzz() { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Uri.EMPTY, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - assertFalse(mService.shouldMuteNotificationLocked(ringtoneNotification)); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyVibrateLooped(); - - NotificationRecord interrupter = getBuzzyOtherNotification(); - assertTrue(mService.shouldMuteNotificationLocked(interrupter)); - mService.buzzBeepBlinkLocked(interrupter); - - verifyVibrate(1); - - assertFalse(interrupter.isInterruptive()); - assertEquals(-1, interrupter.getLastAudiblyAlertedMs()); - } - - @Test - public void testCanInterruptRingtoneNonInsistentBeep() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, false); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepUnlooped(); - - NotificationRecord interrupter = getBeepyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyBeep(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testCanInterruptRingtoneNonInsistentBuzz() { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(null, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, false); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyVibrate(); - - NotificationRecord interrupter = getBuzzyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyVibrate(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testRingtoneInsistentBeep_doesNotBlockFutureSoundsOnceStopped() throws Exception { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(Settings.System.DEFAULT_RINGTONE_URI, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - - mService.clearSoundLocked(); - - NotificationRecord interrupter = getBeepyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyBeep(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testRingtoneInsistentBuzz_doesNotBlockFutureSoundsOnceStopped() { - NotificationChannel ringtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - ringtoneChannel.setSound(null, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION_RINGTONE).build()); - ringtoneChannel.enableVibration(true); - NotificationRecord ringtoneNotification = getCallRecord(1, ringtoneChannel, true); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyVibrateLooped(); - - mService.clearVibrateLocked(); - - NotificationRecord interrupter = getBuzzyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyVibrate(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testCanInterruptNonRingtoneInsistentBeep() throws Exception { - NotificationChannel fakeRingtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - NotificationRecord ringtoneNotification = getCallRecord(1, fakeRingtoneChannel, true); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - verifyBeepLooped(); - - NotificationRecord interrupter = getBeepyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyBeep(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testCanInterruptNonRingtoneInsistentBuzz() { - NotificationChannel fakeRingtoneChannel = - new NotificationChannel("ringtone", "", IMPORTANCE_HIGH); - fakeRingtoneChannel.enableVibration(true); - fakeRingtoneChannel.setSound(null, - new AudioAttributes.Builder().setUsage(USAGE_NOTIFICATION).build()); - NotificationRecord ringtoneNotification = getCallRecord(1, fakeRingtoneChannel, true); - - mService.buzzBeepBlinkLocked(ringtoneNotification); - - NotificationRecord interrupter = getBuzzyOtherNotification(); - mService.buzzBeepBlinkLocked(interrupter); - - verifyVibrate(2); - - assertTrue(interrupter.isInterruptive()); - } - - @Test - public void testBubbleSuppressedNotificationDoesntMakeSound() { - Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( - mock(PendingIntent.class), mock(Icon.class)) - .build(); - - NotificationRecord record = getBuzzyNotification(); - metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - record.getNotification().setBubbleMetadata(metadata); - record.setAllowBubble(true); - record.getNotification().flags |= FLAG_BUBBLE; - record.isUpdate = true; - record.setInterruptive(false); - - mService.buzzBeepBlinkLocked(record); - verifyNeverVibrate(); - } - - @Test - public void testOverflowBubbleSuppressedNotificationDoesntMakeSound() { - Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( - mock(PendingIntent.class), mock(Icon.class)) - .build(); - - NotificationRecord record = getBuzzyNotification(); - metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - record.getNotification().setBubbleMetadata(metadata); - record.setFlagBubbleRemoved(true); - record.setAllowBubble(true); - record.isUpdate = true; - record.setInterruptive(false); - - mService.buzzBeepBlinkLocked(record); - verifyNeverVibrate(); - } - - @Test - public void testBubbleUpdateMakesSound() { - Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( - mock(PendingIntent.class), mock(Icon.class)) - .build(); - - NotificationRecord record = getBuzzyNotification(); - record.getNotification().setBubbleMetadata(metadata); - record.setAllowBubble(true); - record.getNotification().flags |= FLAG_BUBBLE; - record.isUpdate = true; - record.setInterruptive(true); - - mService.buzzBeepBlinkLocked(record); - verifyVibrate(1); - } - - @Test - public void testNewBubbleSuppressedNotifMakesSound() { - Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder( - mock(PendingIntent.class), mock(Icon.class)) - .build(); - - NotificationRecord record = getBuzzyNotification(); - metadata.setFlags(Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - record.getNotification().setBubbleMetadata(metadata); - record.setAllowBubble(true); - record.getNotification().flags |= FLAG_BUBBLE; - record.isUpdate = false; - record.setInterruptive(true); - - mService.buzzBeepBlinkLocked(record); - verifyVibrate(1); - } - - @Test - public void testStartFlashNotificationEvent_receiveBeepyNotification() throws Exception { - NotificationRecord r = getBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepUnlooped(); - verifyNeverVibrate(); - verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(), - eq(r.getSbn().getPackageName())); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testStartFlashNotificationEvent_receiveBuzzyNotification() throws Exception { - NotificationRecord r = getBuzzyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - verifyVibrate(); - verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(), - eq(r.getSbn().getPackageName())); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testStartFlashNotificationEvent_receiveBuzzyBeepyNotification() throws Exception { - NotificationRecord r = getBuzzyBeepyNotification(); - - mService.buzzBeepBlinkLocked(r); - - verifyBeepUnlooped(); - verifyDelayedVibrate(r.getVibration()); - verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(), - eq(r.getSbn().getPackageName())); - assertTrue(r.isInterruptive()); - assertNotEquals(-1, r.getLastAudiblyAlertedMs()); - } - - @Test - public void testStartFlashNotificationEvent_receiveBuzzyBeepyNotification_ringerModeSilent() - throws Exception { - NotificationRecord r = getBuzzyBeepyNotification(); - when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(0); - when(mAudioManager.shouldNotificationSoundPlay(any())).thenReturn(false); - - mService.buzzBeepBlinkLocked(r); - - verifyNeverBeep(); - verifyNeverVibrate(); - verify(mAccessibilityService).startFlashNotificationEvent(any(), anyInt(), - eq(r.getSbn().getPackageName())); - assertFalse(r.isInterruptive()); - assertEquals(-1, r.getLastAudiblyAlertedMs()); - } - - static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> { - private final int mRepeatIndex; - - VibrateRepeatMatcher(int repeatIndex) { - mRepeatIndex = repeatIndex; - } - - @Override - public boolean matches(VibrationEffect actual) { - if (actual instanceof VibrationEffect.Composed - && ((VibrationEffect.Composed) actual).getRepeatIndex() == mRepeatIndex) { - return true; - } - // All non-waveform effects are essentially one shots. - return mRepeatIndex == -1; - } - - @Override - public String toString() { - return "repeatIndex=" + mRepeatIndex; - } - } -} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index 05b6c907069b..e5c42082ab97 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -20,10 +20,12 @@ import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND; import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT; import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE; +import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled; import static com.google.common.truth.Truth.assertThat; @@ -1803,7 +1805,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Test public void testInfoIsPermittedForProfile_notProfile() { - when(mUserProfiles.isProfileUser(anyInt())).thenReturn(false); + when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(false); IInterface service = mock(IInterface.class); when(service.asBinder()).thenReturn(mock(IBinder.class)); @@ -1817,7 +1819,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Test public void testInfoIsPermittedForProfile_profileAndDpmAllows() { - when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true); + when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(true); when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(true); IInterface service = mock(IInterface.class); @@ -1833,7 +1835,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Test public void testInfoIsPermittedForProfile_profileAndDpmDenies() { - when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true); + when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(true); when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(false); IInterface service = mock(IInterface.class); @@ -1853,20 +1855,29 @@ public class ManagedServicesTest extends UiServiceTestCase { UserInfo profile = new UserInfo(ActivityManager.getCurrentUser(), "current", 0); profile.userType = USER_TYPE_FULL_SECONDARY; users.add(profile); - UserInfo managed = new UserInfo(12, "12", 0); + UserInfo managed = new UserInfo(12, "12", UserInfo.FLAG_PROFILE); managed.userType = USER_TYPE_PROFILE_MANAGED; users.add(managed); - UserInfo clone = new UserInfo(13, "13", 0); + UserInfo clone = new UserInfo(13, "13", UserInfo.FLAG_PROFILE); clone.userType = USER_TYPE_PROFILE_CLONE; users.add(clone); + UserInfo privateProfile = new UserInfo(14, "14", UserInfo.FLAG_PROFILE); + if (privateSpaceFlagsEnabled()) { + privateProfile.userType = USER_TYPE_PROFILE_PRIVATE; + users.add(privateProfile); + } when(mUm.getProfiles(ActivityManager.getCurrentUser())).thenReturn(users); + when(mUm.getProfileParent(anyInt())).thenReturn(new UserInfo(0, "primary", 0)); ManagedServices.UserProfiles profiles = new ManagedServices.UserProfiles(); profiles.updateCache(mContext); - assertFalse(profiles.isProfileUser(ActivityManager.getCurrentUser())); - assertTrue(profiles.isProfileUser(12)); - assertTrue(profiles.isProfileUser(13)); + assertFalse(profiles.isProfileUser(ActivityManager.getCurrentUser(), mContext)); + assertTrue(profiles.isProfileUser(12, mContext)); + assertTrue(profiles.isProfileUser(13, mContext)); + if (privateSpaceFlagsEnabled()) { + assertTrue(profiles.isProfileUser(14, mContext)); + } } @Test @@ -2015,7 +2026,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Test public void isComponentEnabledForCurrentProfiles_profileUserId() { final int profileUserId = 10; - when(mUserProfiles.isProfileUser(profileUserId)).thenReturn(true); + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); // Only approve for parent user (0) mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true); @@ -2028,7 +2039,7 @@ public class ManagedServicesTest extends UiServiceTestCase { @Test public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() { final int profileUserId = 10; - when(mUserProfiles.isProfileUser(profileUserId)).thenReturn(true); + when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true); // Do not rebind for parent users (NAS use-case) ManagedServices service = spy(mService); when(service.allowRebindForParentUser()).thenReturn(false); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index a1c24f1f27bf..acac63cd19f8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -210,7 +210,6 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertTrue(mAccessibilityManager.isEnabled()); // TODO (b/291907312): remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); // Disable feature flags by default. Tests should enable as needed. mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED); @@ -2486,6 +2485,17 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } } + @Test + public void testSoundResetsRankingTime() throws Exception { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_UPDATE_RANKING_TIME); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + initAttentionHelper(flagResolver); + + NotificationRecord r = getBuzzyBeepyNotification(); + mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS); + assertThat(r.getRankingTimeMs()).isEqualTo(r.getSbn().getPostTime()); + } + static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> { private final int mRepeatIndex; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 06a4ac932e8e..ef879eeec112 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_ import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.SHOW_IMMEDIATELY; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.Flags.FLAG_KEYGUARD_PRIVATE_NOTIFICATIONS; +import static android.app.Flags.FLAG_UPDATE_RANKING_TIME; import static android.app.Notification.EXTRA_ALLOW_DURING_SETUP; import static android.app.Notification.EXTRA_PICTURE; import static android.app.Notification.EXTRA_PICTURE_ICON; @@ -81,6 +82,7 @@ import static android.os.UserHandle.USER_SYSTEM; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS; @@ -101,7 +103,6 @@ import static android.service.notification.NotificationListenerService.Ranking.U import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; - import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; @@ -111,11 +112,11 @@ import static com.android.server.notification.NotificationManagerService.DEFAULT import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED; import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED; - import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; - +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -124,7 +125,6 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; - import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.isNull; @@ -132,27 +132,7 @@ import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; +import static org.mockito.Mockito.*; import android.Manifest; import android.annotation.Nullable; @@ -207,7 +187,6 @@ import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Icon; import android.media.AudioManager; -import android.media.IRingtonePlayer; import android.media.session.MediaSession; import android.net.Uri; import android.os.Binder; @@ -246,7 +225,6 @@ import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import android.telecom.TelecomManager; -import android.telephony.TelephonyManager; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; @@ -260,10 +238,8 @@ import android.util.AtomicFile; import android.util.Pair; import android.util.Xml; import android.widget.RemoteViews; - import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; - import com.android.internal.R; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.config.sysui.TestableFlagResolver; @@ -294,13 +270,10 @@ import com.android.server.uri.UriGrantsManagerInternal; import com.android.server.utils.quota.MultiRateLimiter; import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; - import com.google.android.collect.Lists; import com.google.common.collect.ImmutableList; - import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; - import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -439,6 +412,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private NotificationChannel mTestNotificationChannel = new NotificationChannel( TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); + NotificationChannel mSilentChannel = new NotificationChannel("low", "low", IMPORTANCE_LOW); + private static final int NOTIFICATION_LOCATION_UNKNOWN = 0; private static final String VALID_CONVO_SHORTCUT_ID = "shortcut"; @@ -494,6 +469,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock StatusBarManagerInternal mStatusBar; + @Mock + NotificationAttentionHelper mAttentionHelper; + private NotificationManagerService.WorkerHandler mWorkerHandler; private class TestableToastCallback extends ITransientNotification.Stub { @@ -661,7 +639,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // TODO (b/291907312): remove feature flag // NOTE: Prefer using the @EnableFlag annotation where possible. Do not add any android.app // flags here. - mSetFlagsRule.disableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER, + mSetFlagsRule.disableFlags( Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE); initNMS(); @@ -695,11 +673,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAtm, mAppUsageStats, mDevicePolicyManager, mUgm, mUgmInternal, mAppOpsManager, mUm, mHistoryManager, mStatsManager, - mock(TelephonyManager.class), mAmi, mToastRateLimiter, mPermissionHelper, mock(UsageStatsManagerInternal.class), mTelecomManager, mLogger, mTestFlagResolver, mPermissionManager, mPowerManager, mPostNotificationTrackerFactory); + mService.setAttentionHelper(mAttentionHelper); + // Return first true for RoleObserver main-thread check when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false); @@ -715,13 +694,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mHistoryManager).onBootPhaseAppsCanStart(); } - // TODO b/291907312: remove feature flag - if (Flags.refactorAttentionHelper()) { - mService.mAttentionHelper.setAudioManager(mAudioManager); - } else { - mService.setAudioManager(mAudioManager); - } - mStrongAuthTracker = mService.new StrongAuthTrackerFake(mContext); mService.setStrongAuthTracker(mStrongAuthTracker); @@ -793,14 +765,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService = mService.getBinderService(); mInternalService = mService.getInternalService(); - mBinderService.createNotificationChannels( - PKG, new ParceledListSlice(Arrays.asList(mTestNotificationChannel))); - mBinderService.createNotificationChannels( - PKG_P, new ParceledListSlice(Arrays.asList(mTestNotificationChannel))); - mBinderService.createNotificationChannels( - PKG_O, new ParceledListSlice(Arrays.asList(mTestNotificationChannel))); + mBinderService.createNotificationChannels(PKG, new ParceledListSlice( + Arrays.asList(mTestNotificationChannel, mSilentChannel))); + mBinderService.createNotificationChannels(PKG_P, new ParceledListSlice( + Arrays.asList(mTestNotificationChannel, mSilentChannel))); + mBinderService.createNotificationChannels(PKG_O, new ParceledListSlice( + Arrays.asList(mTestNotificationChannel, mSilentChannel))); assertNotNull(mBinderService.getNotificationChannel( PKG, mContext.getUserId(), PKG, TEST_CHANNEL_ID)); + assertNotNull(mBinderService.getNotificationChannel( + PKG, mContext.getUserId(), PKG, mSilentChannel.getId())); clearInvocations(mRankingHandler); when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); @@ -1041,11 +1015,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, int userId) { + return generateNotificationRecord(channel, id, userId, "foo"); + } + + private NotificationRecord generateNotificationRecord(NotificationChannel channel, int id, + int userId, String title) { if (channel == null) { channel = mTestNotificationChannel; } Notification.Builder nb = new Notification.Builder(mContext, channel.getId()) - .setContentTitle("foo") + .setContentTitle(title) .setSmallIcon(android.R.drawable.sym_def_app_icon); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, id, "tag", mUid, 0, nb.build(), new UserHandle(userId), null, 0); @@ -1811,23 +1790,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testEnqueueNotificationWithTag_WritesExpectedLogs_NAHRefactor() throws Exception { - // TODO b/291907312: remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); - // Cleanup NMS before re-initializing - if (mService != null) { - try { - mService.onDestroy(); - } catch (IllegalStateException | IllegalArgumentException e) { - // can throw if a broadcast receiver was never registered - } - } - initNMS(); - - testEnqueueNotificationWithTag_WritesExpectedLogs(); - } - - @Test public void testEnqueueNotificationWithTag_LogsOnMajorUpdates() throws Exception { final String tag = "testEnqueueNotificationWithTag_LogsOnMajorUpdates"; Notification original = new Notification.Builder(mContext, @@ -5619,8 +5581,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />" + "</dnd_apps>" + "</notification-policy>"; - UserInfo ui = new UserInfo(); - ui.id = 10; + UserInfo ui = new UserInfo(10, "Clone", UserInfo.FLAG_PROFILE); ui.userType = USER_TYPE_PROFILE_CLONE; when(mUmInternal.getUserInfo(10)).thenReturn(ui); mService.readPolicyXml( @@ -5646,8 +5607,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />" + "</dnd_apps>" + "</notification-policy>"; - UserInfo ui = new UserInfo(); - ui.id = 10; + UserInfo ui = new UserInfo(10, "Work", UserInfo.FLAG_PROFILE); ui.userType = USER_TYPE_PROFILE_MANAGED; when(mUmInternal.getUserInfo(10)).thenReturn(ui); mService.readPolicyXml( @@ -5660,6 +5620,34 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testReadPolicyXml_doesNotRestoreManagedServicesForPrivateUser() throws Exception { + mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES); + final String policyXml = "<notification-policy version=\"1\">" + + "<ranking></ranking>" + + "<enabled_listeners>" + + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />" + + "</enabled_listeners>" + + "<enabled_assistants>" + + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />" + + "</enabled_assistants>" + + "<dnd_apps>" + + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />" + + "</dnd_apps>" + + "</notification-policy>"; + UserInfo ui = new UserInfo(10, "Private", UserInfo.FLAG_PROFILE); + ui.userType = USER_TYPE_PROFILE_PRIVATE; + when(mUmInternal.getUserInfo(10)).thenReturn(ui); + mService.readPolicyXml( + new BufferedInputStream(new ByteArrayInputStream(policyXml.getBytes())), + true, + 10); + verify(mListeners, never()).readXml(any(), any(), eq(true), eq(10)); + verify(mConditionProviders, never()).readXml(any(), any(), eq(true), eq(10)); + verify(mAssistants, never()).readXml(any(), any(), eq(true), eq(10)); + } + + @Test public void testReadPolicyXml_restoresManagedServicesForNonManagedUser() throws Exception { final String policyXml = "<notification-policy version=\"1\">" + "<ranking></ranking>" @@ -10105,13 +10093,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped() throws RemoteException { - IRingtonePlayer mockPlayer = mock(IRingtonePlayer.class); - when(mAudioManager.getRingtonePlayer()).thenReturn(mockPlayer); - // Set up volume to be above 0, and for AudioManager to signal playback should happen, - // for the sound to actually play - when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10); - when(mAudioManager.shouldNotificationSoundPlay(any(android.media.AudioAttributes.class))) - .thenReturn(true); setUpPrefsForBubbles(PKG, mUid, true /* global */, @@ -10130,25 +10111,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { waitForIdle(); // Check audio is stopped - verify(mockPlayer).stopAsync(); - } - - @Test - public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped_NAHRefactor() - throws Exception { - // TODO b/291907312: remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); - // Cleanup NMS before re-initializing - if (mService != null) { - try { - mService.onDestroy(); - } catch (IllegalStateException | IllegalArgumentException e) { - // can throw if a broadcast receiver was never registered - } - } - initNMS(); - - testOnBubbleMetadataChangedToSuppressNotification_soundStopped(); + verify(mAttentionHelper).clearEffectsLocked(nr.getKey()); } @Test @@ -14775,6 +14738,110 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(listener, never()).onCallNotificationRemoved(anyString(), any()); } + @Test + @EnableFlags(FLAG_UPDATE_RANKING_TIME) + public void rankingTime_newNotification_noisy_matchesSbn() throws Exception { + NotificationRecord nr = generateNotificationRecord(mTestNotificationChannel, mUserId); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + + NotificationRecord posted = mService.mNotificationList.get(0); + long originalPostTime = posted.getSbn().getPostTime(); + assertThat(posted.getRankingTimeMs()).isEqualTo(originalPostTime); + } + + @Test + @EnableFlags(FLAG_UPDATE_RANKING_TIME) + public void rankingTime_newNotification_silent_matchesSbn() throws Exception { + NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); + NotificationRecord nr = generateNotificationRecord(low, mUserId); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + + NotificationRecord posted = mService.mNotificationList.get(0); + long originalPostTime = posted.getSbn().getPostTime(); + assertThat(posted.getRankingTimeMs()).isEqualTo(originalPostTime); + } + + @Test + @EnableFlags(FLAG_UPDATE_RANKING_TIME) + public void rankingTime_updatedNotification_silentSameText_originalPostTime() throws Exception { + NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); + NotificationRecord nr = generateNotificationRecord(low, mUserId); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + NotificationRecord posted = mService.mNotificationList.get(0); + long originalPostTime = posted.getSbn().getPostTime(); + assertThat(posted.getRankingTimeMs()).isEqualTo(originalPostTime); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + assertThat(mService.mNotificationList.get(0).getRankingTimeMs()) + .isEqualTo(originalPostTime); + } + + @Test + @EnableFlags(FLAG_UPDATE_RANKING_TIME) + public void rankingTime_updatedNotification_silentNewText_newPostTime() throws Exception { + NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); + NotificationRecord nr = generateNotificationRecord(low, 0, mUserId); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + NotificationRecord posted = mService.mNotificationList.get(0); + long originalPostTime = posted.getSbn().getPostTime(); + assertThat(posted.getRankingTimeMs()).isEqualTo(originalPostTime); + + NotificationRecord nrUpdate = generateNotificationRecord(low, 0, mUserId, "bar"); + // no attention helper mocked behavior needed because this does not make noise + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nrUpdate.getSbn().getId(), nrUpdate.getSbn().getNotification(), + nrUpdate.getSbn().getUserId()); + waitForIdle(); + + posted = mService.mNotificationList.get(0); + assertThat(posted.getRankingTimeMs()).isGreaterThan(originalPostTime); + assertThat(posted.getRankingTimeMs()).isEqualTo(posted.getSbn().getPostTime()); + } + + @Test + @EnableFlags(FLAG_UPDATE_RANKING_TIME) + public void rankingTime_updatedNotification_noisySameText_newPostTime() throws Exception { + NotificationChannel low = new NotificationChannel("low", "low", IMPORTANCE_LOW); + NotificationRecord nr = generateNotificationRecord(low, mUserId); + + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); + waitForIdle(); + NotificationRecord posted = mService.mNotificationList.get(0); + long originalPostTime = posted.getSbn().getPostTime(); + assertThat(posted.getRankingTimeMs()).isEqualTo(originalPostTime); + + NotificationRecord nrUpdate = generateNotificationRecord(mTestNotificationChannel, mUserId); + when(mAttentionHelper.buzzBeepBlinkLocked(any(), any())).thenAnswer(new Answer<Object>() { + public Object answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + ((NotificationRecord) args[0]).resetRankingTime(); + return 2; // beep + } + }); + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag0", + nrUpdate.getSbn().getId(), nrUpdate.getSbn().getNotification(), + nrUpdate.getSbn().getUserId()); + waitForIdle(); + posted = mService.mNotificationList.get(0); + assertThat(posted.getRankingTimeMs()).isGreaterThan(originalPostTime); + assertThat(posted.getRankingTimeMs()).isEqualTo(posted.getSbn().getPostTime()); + } + private NotificationRecord createAndPostCallStyleNotification(String packageName, UserHandle userHandle, String testName) throws Exception { Person person = new Person.Builder().setName("caller").build(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java index 70910b108fe9..c1bb3e7408fc 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java @@ -164,7 +164,7 @@ public class RoleObserverTest extends UiServiceTestCase { mock(DevicePolicyManagerInternal.class), mock(IUriGrantsManager.class), mock(UriGrantsManagerInternal.class), mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class), - mock(StatsManager.class), mock(TelephonyManager.class), + mock(StatsManager.class), mock(ActivityManagerInternal.class), mock(MultiRateLimiter.class), mock(PermissionHelper.class), mock(UsageStatsManagerInternal.class), mock(TelecomManager.class), diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 0b76154d73d0..19ce217e581c 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -251,9 +251,9 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1); - CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.runAsync(() -> { - mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f); - }); + mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f); + CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.completedFuture( + null); long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture, USAGE_RINGTONE); waitForCompletion(); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index 5861d88924e0..185677f966a4 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -1589,7 +1589,8 @@ public class VibratorManagerServiceTest { assertEquals(1f, ((PrimitiveSegment) segments.get(2)).getScale(), 1e-5); verify(mVibratorFrameworkStatsLoggerMock).logVibrationAdaptiveHapticScale(UID, 0.7f); verify(mVibratorFrameworkStatsLoggerMock).logVibrationAdaptiveHapticScale(UID, 0.4f); - verify(mVibratorFrameworkStatsLoggerMock).logVibrationAdaptiveHapticScale(UID, 1f); + verify(mVibratorFrameworkStatsLoggerMock, + timeout(TEST_TIMEOUT_MILLIS)).logVibrationAdaptiveHapticScale(UID, 1f); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index ff7129c50459..018600641853 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -64,8 +64,10 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -2368,9 +2370,7 @@ public class TransitionTests extends WindowTestsBase { assertTrue(transitA.isCollecting()); // finish collecting A - transitA.start(); - transitA.setAllReady(); - mSyncEngine.tryFinishForTest(transitA.getSyncId()); + tryFinishTransitionSyncSet(transitA); waitUntilHandlersIdle(); assertTrue(transitA.isPlaying()); @@ -2476,6 +2476,36 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testDeferredMoveTaskToBack() { + final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task task = activity.getTask(); + registerTestTransitionPlayer(); + final TransitionController controller = mWm.mRoot.mTransitionController; + mSyncEngine = createTestBLASTSyncEngine(); + controller.setSyncEngine(mSyncEngine); + final Transition transition = createTestTransition(TRANSIT_CHANGE, controller); + controller.moveToCollecting(transition); + task.moveTaskToBack(task); + // Actual action will be deferred by current transition. + verify(task, never()).moveToBack(any(), any()); + + tryFinishTransitionSyncSet(transition); + waitUntilHandlersIdle(); + // Continue to move task to back after the transition is done. + verify(task).moveToBack(any(), any()); + final Transition moveBackTransition = controller.getCollectingTransition(); + assertNotNull(moveBackTransition); + moveBackTransition.abort(); + + // The move-to-back can be collected in to a collecting OPEN transition. + clearInvocations(task); + final Transition transition2 = createTestTransition(TRANSIT_OPEN, controller); + controller.moveToCollecting(transition2); + task.moveTaskToBack(task); + verify(task).moveToBack(any(), any()); + } + + @Test public void testNoSyncFlagIfOneTrack() { final TransitionController controller = mAtm.getTransitionController(); final TestTransitionPlayer player = registerTestTransitionPlayer(); @@ -2492,17 +2522,11 @@ public class TransitionTests extends WindowTestsBase { controller.startCollectOrQueue(transitC, (deferred) -> {}); // Verify that, as-long as there is <= 1 track, we won't get a SYNC flag - transitA.start(); - transitA.setAllReady(); - mSyncEngine.tryFinishForTest(transitA.getSyncId()); + tryFinishTransitionSyncSet(transitA); assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); - transitB.start(); - transitB.setAllReady(); - mSyncEngine.tryFinishForTest(transitB.getSyncId()); + tryFinishTransitionSyncSet(transitB); assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); - transitC.start(); - transitC.setAllReady(); - mSyncEngine.tryFinishForTest(transitC.getSyncId()); + tryFinishTransitionSyncSet(transitC); assertTrue((player.mLastReady.getFlags() & FLAG_SYNC) == 0); } @@ -2642,6 +2666,12 @@ public class TransitionTests extends WindowTestsBase { assertEquals("reason1", condition1.mAlternate); } + private void tryFinishTransitionSyncSet(Transition transition) { + transition.setAllReady(); + transition.start(); + mSyncEngine.tryFinishForTest(transition.getSyncId()); + } + private static void makeTaskOrganized(Task... tasks) { final ITaskOrganizer organizer = mock(ITaskOrganizer.class); for (Task t : tasks) { |