diff options
169 files changed, 5469 insertions, 6002 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 3391698ee15a..233fb8a5914b 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -631,6 +631,12 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +cc_aconfig_library { + name: "android.database.sqlite-aconfig-cc", + aconfig_declarations: "android.database.sqlite-aconfig", + host_supported: true, +} + // Biometrics aconfig_declarations { name: "android.hardware.biometrics.flags-aconfig", @@ -664,6 +670,11 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +cc_aconfig_library { + name: "android.server.display.flags-aconfig-cc", + aconfig_declarations: "display_flags", +} + java_aconfig_library { name: "com.android.internal.foldables.flags-aconfig-java", aconfig_declarations: "fold_lock_setting_flags", diff --git a/core/api/current.txt b/core/api/current.txt index 982ab640af7c..93fa9a0e8742 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -57034,7 +57034,6 @@ package android.view.textclassifier { field public static final String TYPE_EMAIL = "email"; field public static final String TYPE_FLIGHT_NUMBER = "flight"; field public static final String TYPE_OTHER = "other"; - field @FlaggedApi("android.service.notification.redact_sensitive_notifications_from_untrusted_listeners") public static final String TYPE_OTP_CODE = "otp_code"; field public static final String TYPE_PHONE = "phone"; field public static final String TYPE_UNKNOWN = ""; field public static final String TYPE_URL = "url"; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 0ed25eb3125a..ff713d071a05 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -2685,7 +2685,8 @@ public class AppOpsManager { .setDefaultMode(getSystemAlertWindowDefault()).build(), new AppOpInfo.Builder(OP_ACCESS_NOTIFICATIONS, OPSTR_ACCESS_NOTIFICATIONS, "ACCESS_NOTIFICATIONS") - .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS).build(), + .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) + .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), new AppOpInfo.Builder(OP_CAMERA, OPSTR_CAMERA, "CAMERA") .setPermission(android.Manifest.permission.CAMERA) .setRestriction(UserManager.DISALLOW_CAMERA) 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/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 2c26389071ce..5e00b7a798d8 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.legacyStartObservingDevicePresence(deviceAddress, + mService.registerDevicePresenceListenerService(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.legacyStopObservingDevicePresence(deviceAddress, + mService.unregisterDevicePresenceListenerService(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.notifySelfManagedDeviceAppeared(associationId); + mService.notifyDeviceAppeared(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.notifySelfManagedDeviceDisappeared(associationId); + mService.notifyDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 1b00f90e1fb3..57d59e5e5bf0 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -59,16 +59,12 @@ interface ICompanionDeviceManager { int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - 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); + void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage, + int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); + void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage, + int userId); boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId); @@ -97,11 +93,9 @@ interface ICompanionDeviceManager { @EnforcePermission("USE_COMPANION_TRANSPORTS") void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); - @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") - void notifySelfManagedDeviceAppeared(int associationId); + void notifyDeviceAppeared(int associationId); - @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") - void notifySelfManagedDeviceDisappeared(int associationId); + void notifyDeviceDisappeared(int associationId); PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId, int associationId); @@ -141,4 +135,10 @@ 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/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 41f093614e6c..4c0da7c98488 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -267,6 +267,15 @@ public abstract class PackageManager { "android.app.PROPERTY_LEGACY_UPDATE_OWNERSHIP_DENYLIST"; /** + * Application level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for a app to inform the installer that a file containing the app's android + * safety label data is bundled into the APK at the given path. + * @hide + */ + public static final String PROPERTY_ANDROID_SAFETY_LABEL_PATH = + "android.content.SAFETY_LABEL_PATH"; + + /** * A property value set within the manifest. * <p> * The value of a property will only have a single type, as defined by diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 311e99111d04..f26a797f0c12 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -98,6 +98,46 @@ flag { } flag { + name: "adpf_use_fmq_channel_fixed" + namespace: "game" + description: "Guards use of the FMQ channel for ADPF with a readonly flag" + is_fixed_read_only: true + bug: "315894228" +} + +flag { + name: "adpf_fmq_eager_send" + namespace: "game" + description: "Guards the use of an eager-sending optimization in FMQ for low-latency messages" + is_fixed_read_only: true + bug: "315894228" +} + +flag { + name: "adpf_hwui_gpu" + namespace: "game" + description: "Guards use of the FMQ channel for ADPF" + is_fixed_read_only: true + bug: "330922490" +} + +flag { + name: "adpf_obtainview_boost" + namespace: "game" + description: "Guards use of a boost in response to HWUI obtainView" + is_fixed_read_only: true + bug: "328238660" +} + +flag { + name: "adpf_platform_power_efficiency" + namespace: "game" + description: "Guards use of the ADPF power efficiency API within the platform" + is_fixed_read_only: true + bug: "277285195" +} + +flag { name: "battery_service_support_current_adb_command" namespace: "backstage_power" description: "Whether or not BatteryService supports adb commands for Current values." diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 1d2f65353a65..ef5004536354 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -16,9 +16,6 @@ package android.view.textclassifier; -import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; - -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -111,9 +108,6 @@ public interface TextClassifier { String TYPE_DATE_TIME = "datetime"; /** Flight number in IATA format. */ String TYPE_FLIGHT_NUMBER = "flight"; - /** One-time login codes */ - @FlaggedApi(FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS) - String TYPE_OTP_CODE = "otp_code"; /** * Word that users may be interested to look up for meaning. * @hide @@ -132,8 +126,7 @@ public interface TextClassifier { TYPE_DATE, TYPE_DATE_TIME, TYPE_FLIGHT_NUMBER, - TYPE_DICTIONARY, - TYPE_OTP_CODE + TYPE_DICTIONARY }) @interface EntityType {} diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 5f688f6406bc..99b3f9a16355 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -43,14 +43,16 @@ interface IBatteryStats { void noteStartVideo(int uid); @EnforcePermission("UPDATE_DEVICE_STATS") void noteStopVideo(int uid); + // The audio battery stats interface is oneway to prevent inversion. These calls + // are ordered with respect to each other, but not with any other calls. @EnforcePermission("UPDATE_DEVICE_STATS") - void noteStartAudio(int uid); + oneway void noteStartAudio(int uid); @EnforcePermission("UPDATE_DEVICE_STATS") - void noteStopAudio(int uid); + oneway void noteStopAudio(int uid); @EnforcePermission("UPDATE_DEVICE_STATS") void noteResetVideo(); @EnforcePermission("UPDATE_DEVICE_STATS") - void noteResetAudio(); + oneway void noteResetAudio(); @EnforcePermission("UPDATE_DEVICE_STATS") void noteFlashlightOn(int uid); @EnforcePermission("UPDATE_DEVICE_STATS") diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index e1aff2af0965..b316a01c335a 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -421,8 +421,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @NonNull private String[] mUsesStaticLibrariesSorted; - private boolean mAppMetadataFileInApk = false; - @NonNull public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath, @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp, @@ -1065,11 +1063,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return memtagMode; } - @Override - public boolean isAppMetadataFileInApk() { - return mAppMetadataFileInApk; - } - @Nullable @Override public Bundle getMetaData() { @@ -2158,12 +2151,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setAppMetadataFileInApk(boolean fileInApk) { - mAppMetadataFileInApk = fileInApk; - return this; - } - - @Override public PackageImpl setMetaData(@Nullable Bundle value) { metaData = value; return this; @@ -3277,7 +3264,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, dest.writeLong(this.mBooleans); dest.writeLong(this.mBooleans2); dest.writeBoolean(this.mAllowCrossUidActivitySwitchFromBelow); - dest.writeBoolean(this.mAppMetadataFileInApk); } public PackageImpl(Parcel in) { @@ -3445,7 +3431,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, this.mBooleans = in.readLong(); this.mBooleans2 = in.readLong(); this.mAllowCrossUidActivitySwitchFromBelow = in.readBoolean(); - this.mAppMetadataFileInApk = in.readBoolean(); assignDerivedFields(); assignDerivedFields2(); diff --git a/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java b/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java index 3c26a7c3eba3..66cfb69d7871 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java +++ b/core/java/com/android/internal/pm/parsing/pkg/ParsedPackage.java @@ -127,7 +127,4 @@ public interface ParsedPackage extends AndroidPackage { ParsedPackage setDirectBootAware(boolean directBootAware); ParsedPackage setPersistent(boolean persistent); - - /** Retrieves whether the apk contains a app metadata file. */ - boolean isAppMetadataFileInApk(); } diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java index 5ab17a6b2b19..5d185af17d48 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java @@ -133,9 +133,6 @@ public interface ParsingPackage { @Nullable SparseArray<int[]> splitDependencies ); - /** Sets whether the apk contains a app metadata file. */ - ParsingPackage setAppMetadataFileInApk(boolean fileInApk); - ParsingPackage setMetaData(Bundle metaData); ParsingPackage setForceQueryable(boolean forceQueryable); diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 95ecd47e3037..9df93f9a50f7 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -46,7 +46,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.FeatureGroupInfo; import android.content.pm.FeatureInfo; -import android.content.pm.Flags; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.Property; @@ -134,7 +133,6 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.security.PublicKey; @@ -165,8 +163,6 @@ public class ParsingPackageUtils { */ public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; - public static final String APP_METADATA_FILE_NAME = "app.metadata"; - /** * Path prefix for apps on expanded storage */ @@ -640,12 +636,6 @@ public class ParsingPackageUtils { pkg.setSigningDetails(SigningDetails.UNKNOWN); } - if (Flags.aslInApkAppMetadataSource()) { - try (InputStream in = assets.open(APP_METADATA_FILE_NAME)) { - pkg.setAppMetadataFileInApk(true); - } catch (Exception e) { } - } - return input.success(pkg); } catch (Exception e) { return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, 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/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index 65d5a1fdbd00..e5ef8333e134 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -250,14 +250,6 @@ public class EmphasizedNotificationButton extends Button { return; } - if (mIconToGlue == null && mLabelToGlue == null) { - if (DEBUG_NEW_ACTION_LAYOUT) { - Log.v(TAG, "glueIconAndLabelIfNeeded: no icon or label to glue; doing nothing"); - } - mGluePending = false; - return; - } - if (!evenlyDividedCallStyleActionLayout()) { Log.e(TAG, "glueIconAndLabelIfNeeded: new action layout disabled; doing nothing"); return; @@ -273,17 +265,6 @@ public class EmphasizedNotificationButton extends Button { return; } - // Ready to glue but don't have an icon *and* a label: - // - // (Note that this will *not* happen while the button is being initialized, since we won't - // be ready to glue. This can only happen if the button is initialized and displayed and - // *then* someone calls glueIcon or glueLabel. - - if (mLabelToGlue == null) { - Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing"); - return; - } - // Can't glue: final int layoutDirection = getLayoutDirection(); @@ -314,12 +295,26 @@ public class EmphasizedNotificationButton extends Button { private static final String POP_DIRECTIONAL_ISOLATE = "\u2069"; private void glueIconAndLabel(int layoutDirection) { - if (mIconToGlue == null) { + if (mIconToGlue == null && mLabelToGlue == null) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "glueIconAndLabel: null icon and label, setting text to empty string"); + } + setText(""); + return; + } else if (mIconToGlue == null) { if (DEBUG_NEW_ACTION_LAYOUT) { Log.d(TAG, "glueIconAndLabel: null icon, setting text to label"); } setText(mLabelToGlue); return; + } else if (mLabelToGlue == null) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "glueIconAndLabel: null label, setting text to ImageSpan with icon"); + } + final SpannableStringBuilder builder = new SpannableStringBuilder(); + appendSpan(builder, IMAGE_SPAN_TEXT, new ImageSpan(mIconToGlue, ALIGN_CENTER)); + setText(builder); + return; } final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL; diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 444288c613ea..fd4ff29825a3 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -99,6 +99,7 @@ cc_library_shared_for_libandroid_runtime { "libminikin", "libz", "server_configurable_flags", + "android.database.sqlite-aconfig-cc", "android.media.audiopolicy-aconfig-cc", ], 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/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index faad472d4ad6..365f3bfb1873 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -21,7 +21,7 @@ import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; -import static android.view.stylus.HandwritingTestUtil.createView; +import static android.view.stylus.HandwritingTestUtil.createEditText; import static com.android.text.flags.Flags.handwritingCursorPosition; @@ -112,13 +112,13 @@ public class HandwritingInitiatorTest { mHandwritingInitiator = spy(new HandwritingInitiator(viewConfiguration, inputMethodManager)); - mTestView1 = createView(sHwArea1, /* autoHandwritingEnabled= */ true, + mTestView1 = createEditText(sHwArea1, /* autoHandwritingEnabled= */ true, /* isStylusHandwritingAvailable= */ true, HW_BOUNDS_OFFSETS_LEFT_PX, HW_BOUNDS_OFFSETS_TOP_PX, HW_BOUNDS_OFFSETS_RIGHT_PX, HW_BOUNDS_OFFSETS_BOTTOM_PX); - mTestView2 = createView(sHwArea2, /* autoHandwritingEnabled= */ true, + mTestView2 = createEditText(sHwArea2, /* autoHandwritingEnabled= */ true, /* isStylusHandwritingAvailable= */ true, HW_BOUNDS_OFFSETS_LEFT_PX, HW_BOUNDS_OFFSETS_TOP_PX, @@ -412,7 +412,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() { final Rect rect = new Rect(600, 600, 900, 900); - final View testView = createView(rect, true /* autoHandwritingEnabled */, + final View testView = createEditText(rect, true /* autoHandwritingEnabled */, false /* isStylusHandwritingAvailable */); mHandwritingInitiator.updateHandwritingAreasForView(testView); @@ -717,7 +717,7 @@ public class HandwritingInitiatorTest { mTestView1.setHandwritingDelegatorCallback(null); onEditorFocusedOrConnectionCreated(mTestView1); } else { - View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */, + View mockView = createEditText(sHwArea1, false /* autoHandwritingEnabled */, true /* isStylusHandwritingAvailable */); onEditorFocusedOrConnectionCreated(mockView); } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java index 3b2ab4c8bb50..2c3ee340cd74 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingTestUtil.java @@ -33,26 +33,63 @@ import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; -public class HandwritingTestUtil { - public static EditText createView(Rect handwritingArea) { +class HandwritingTestUtil { + static View createView(Rect handwritingArea) { return createView(handwritingArea, true /* autoHandwritingEnabled */, true /* isStylusHandwritingAvailable */); } - public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled, + static View createView(Rect handwritingArea, boolean autoHandwritingEnabled, boolean isStylusHandwritingAvailable) { return createView(handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable, 0, 0, 0, 0); } - public static EditText createView(Rect handwritingArea, boolean autoHandwritingEnabled, + static View createView(Rect handwritingArea, boolean autoHandwritingEnabled, boolean isStylusHandwritingAvailable, float handwritingBoundsOffsetLeft, float handwritingBoundsOffsetTop, float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); final Context context = instrumentation.getTargetContext(); - // mock a parent so that HandwritingInitiator can get visible rect and hit region. - final ViewGroup parent = new ViewGroup(context) { + View view = spy(new View(context)); + mockSpy(view, handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable, + handwritingBoundsOffsetLeft, handwritingBoundsOffsetTop, + handwritingBoundsOffsetRight, handwritingBoundsOffsetBottom); + return view; + } + + static EditText createEditText(Rect handwritingArea, boolean autoHandwritingEnabled, + boolean isStylusHandwritingAvailable) { + return createEditText(handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable, + 0, 0, 0, 0); + } + + static EditText createEditText(Rect handwritingArea, boolean autoHandwritingEnabled, + boolean isStylusHandwritingAvailable, + float handwritingBoundsOffsetLeft, float handwritingBoundsOffsetTop, + float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + final Context context = instrumentation.getTargetContext(); + EditText view = spy(new EditText(context)); + doAnswer(invocation -> { + int[] outLocation = invocation.getArgument(0); + outLocation[0] = handwritingArea.left; + outLocation[1] = handwritingArea.top; + return null; + }).when(view).getLocationInWindow(any()); + when(view.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(0); + mockSpy(view, handwritingArea, autoHandwritingEnabled, isStylusHandwritingAvailable, + handwritingBoundsOffsetLeft, handwritingBoundsOffsetTop, + handwritingBoundsOffsetRight, handwritingBoundsOffsetBottom); + return view; + } + + private static void mockSpy(View viewSpy, Rect handwritingArea, + boolean autoHandwritingEnabled, boolean isStylusHandwritingAvailable, + float handwritingBoundsOffsetLeft, float handwritingBoundsOffsetTop, + float handwritingBoundsOffsetRight, float handwritingBoundsOffsetBottom) { + // Mock a parent so that HandwritingInitiator can get visible rect and hit region. + final ViewGroup parent = new ViewGroup(viewSpy.getContext()) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // We don't layout this view. @@ -72,24 +109,15 @@ public class HandwritingTestUtil { } }; - EditText view = spy(new EditText(context)); - when(view.isAttachedToWindow()).thenReturn(true); - when(view.isAggregatedVisible()).thenReturn(true); - when(view.isStylusHandwritingAvailable()).thenReturn(isStylusHandwritingAvailable); - when(view.getHandwritingArea()).thenReturn(handwritingArea); - when(view.getHandwritingBoundsOffsetLeft()).thenReturn(handwritingBoundsOffsetLeft); - when(view.getHandwritingBoundsOffsetTop()).thenReturn(handwritingBoundsOffsetTop); - when(view.getHandwritingBoundsOffsetRight()).thenReturn(handwritingBoundsOffsetRight); - when(view.getHandwritingBoundsOffsetBottom()).thenReturn(handwritingBoundsOffsetBottom); - doAnswer(invocation -> { - int[] outLocation = invocation.getArgument(0); - outLocation[0] = handwritingArea.left; - outLocation[1] = handwritingArea.top; - return null; - }).when(view).getLocationInWindow(any()); - when(view.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(0); - view.setAutoHandwritingEnabled(autoHandwritingEnabled); - parent.addView(view); - return view; + when(viewSpy.isAttachedToWindow()).thenReturn(true); + when(viewSpy.isAggregatedVisible()).thenReturn(true); + when(viewSpy.isStylusHandwritingAvailable()).thenReturn(isStylusHandwritingAvailable); + when(viewSpy.getHandwritingArea()).thenReturn(handwritingArea); + when(viewSpy.getHandwritingBoundsOffsetLeft()).thenReturn(handwritingBoundsOffsetLeft); + when(viewSpy.getHandwritingBoundsOffsetTop()).thenReturn(handwritingBoundsOffsetTop); + when(viewSpy.getHandwritingBoundsOffsetRight()).thenReturn(handwritingBoundsOffsetRight); + when(viewSpy.getHandwritingBoundsOffsetBottom()).thenReturn(handwritingBoundsOffsetBottom); + viewSpy.setAutoHandwritingEnabled(autoHandwritingEnabled); + parent.addView(viewSpy); } } 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..341c3e8cf373 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -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", @@ -539,6 +539,7 @@ cc_defaults { "renderthread/RenderTask.cpp", "renderthread/TimeLord.cpp", "hwui/AnimatedImageDrawable.cpp", + "hwui/AnimatedImageThread.cpp", "hwui/Bitmap.cpp", "hwui/BlurDrawLooper.cpp", "hwui/Canvas.cpp", @@ -599,7 +600,6 @@ cc_defaults { local_include_dirs: ["platform/android"], srcs: [ - "hwui/AnimatedImageThread.cpp", "pipeline/skia/ATraceMemoryDump.cpp", "pipeline/skia/GLFunctorDrawable.cpp", "pipeline/skia/LayerDrawable.cpp", diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp index 078041411a21..8645995e3df1 100644 --- a/libs/hwui/AnimatorManager.cpp +++ b/libs/hwui/AnimatorManager.cpp @@ -90,7 +90,13 @@ void AnimatorManager::pushStaging() { } mCancelAllAnimators = false; } else { - for (auto& animator : mAnimators) { + // create a copy of mAnimators as onAnimatorTargetChanged can erase mAnimators. + FatVector<sp<BaseRenderNodeAnimator>> animators; + animators.reserve(mAnimators.size()); + for (const auto& animator : mAnimators) { + animators.push_back(animator); + } + for (auto& animator : animators) { animator->pushStaging(mAnimationHandle->context()); } } 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/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/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index f36da19afd30..20f1b175e1de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -144,7 +144,8 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { uid, packageName); final boolean ecmEnabled = context.getResources().getBoolean( com.android.internal.R.bool.config_enhancedConfirmationModeEnabled); - return ecmEnabled && mode != AppOpsManager.MODE_ALLOWED; + return ecmEnabled && mode != AppOpsManager.MODE_ALLOWED + && mode != AppOpsManager.MODE_DEFAULT; } catch (Exception e) { // Fallback in case if app ops is not available in testing. return false; diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index 0c54c1903742..45754eb8f16d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -289,7 +289,8 @@ public class RestrictedSwitchPreference extends SwitchPreferenceCompat { uid, packageName); final boolean ecmEnabled = getContext().getResources().getBoolean( com.android.internal.R.bool.config_enhancedConfirmationModeEnabled); - final boolean appOpsAllowed = !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; + final boolean appOpsAllowed = !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED + || mode == AppOpsManager.MODE_DEFAULT; if (!isEnableAllowed && !isEnabled) { setEnabled(false); } else if (isEnabled) { 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/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/res-keyguard/layout/alternate_bouncer.xml b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml index 41fb57a6ebb5..cf9ca157b943 100644 --- a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml +++ b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml @@ -22,8 +22,7 @@ android:focusable="true" android:clickable="true" android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="invisible"> + android:layout_height="match_parent"> <com.android.systemui.scrim.ScrimView android:id="@+id/alternate_bouncer_scrim" diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index c9985354b11e..221b791b8cb5 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -120,10 +120,6 @@ android:inflatedId="@+id/multi_shade" android:layout="@layout/multi_shade" /> - <include layout="@layout/alternate_bouncer" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - <com.android.systemui.biometrics.AuthRippleView android:id="@+id/auth_ripple" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/raw/widget.rec b/packages/SystemUI/res/raw/widget.rec Binary files differnew file mode 100644 index 000000000000..a38b23b0078f --- /dev/null +++ b/packages/SystemUI/res/raw/widget.rec diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryDrawableState.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryDrawableState.kt index 9f13e6d32131..e172cad8f0b8 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryDrawableState.kt +++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryDrawableState.kt @@ -95,8 +95,8 @@ sealed interface BatteryColors { // 22% alpha white override val bg: Int = Color.valueOf(1f, 1f, 1f, 0.22f).toArgb() - // 18% alpha black - override val fill = Color.valueOf(0f, 0f, 0f, 0.18f).toArgb() + // GM Gray 600 + override val fill = Color.parseColor("#80868B") // GM Gray 700 override val fillOnly = Color.parseColor("#5F6368") @@ -115,8 +115,8 @@ sealed interface BatteryColors { // 18% alpha black override val bg: Int = Color.valueOf(0f, 0f, 0f, 0.18f).toArgb() - // 22% alpha white - override val fill = Color.valueOf(1f, 1f, 1f, 0.22f).toArgb() + // GM Gray 700 + override val fill = Color.parseColor("#5F6368") // GM Gray 400 override val fillOnly = Color.parseColor("#BDC1C6") diff --git a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt index 5e34d2909d81..e1ae4980079b 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/battery/unified/BatteryFillDrawable.kt @@ -34,7 +34,7 @@ import kotlin.math.roundToInt /** * Draws a right-to-left fill inside of the given [framePath]. This fill is designed to exactly fill * the usable space inside of [framePath], given that the stroke width of the path is 1.5, and we - * want an extra 0.25 (canvas units) of a gap between the fill and the stroke + * want an extra 0.5 (canvas units) of a gap between the fill and the stroke */ class BatteryFillDrawable(private val framePath: Path) : Drawable() { private var hScale = 1f @@ -61,7 +61,6 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() { private val clearPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> p.style = Paint.Style.STROKE - p.strokeWidth = 5f p.blendMode = BlendMode.CLEAR } @@ -94,6 +93,9 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() { scaledLeftOffset = LeftFillOffset * hScale scaledRightInset = RightFillInset * hScale + + // Ensure 0.5dp space between the frame stroke and the fill + clearPaint.strokeWidth = 2.5f * hScale } override fun draw(canvas: Canvas) { @@ -155,15 +157,15 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() { override fun setAlpha(alpha: Int) {} companion object { - // 3.75f = + // 4f = // 2.75 (left-most edge of the frame path) // + 0.75 (1/2 of the stroke width) - // + 0.25 (padding between stroke and fill edge) - private const val LeftFillOffset = 3.75f + // + 0.5 (padding between stroke and fill edge) + private const val LeftFillOffset = 4f - // 1.75, calculated the same way, but from the right edge (without the battery cap), which + // 2, calculated the same way, but from the right edge (without the battery cap), which // consumes 2 units of width. - private const val RightFillInset = 1.75f + private const val RightFillInset = 2f /** Scale this to the viewport so we fill correctly! */ private val FillRect = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index 307b9856cbb5..20e81c293e40 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -39,6 +39,7 @@ import com.android.systemui.biometrics.udfps.EllipseOverlapDetector import com.android.systemui.biometrics.udfps.OverlapDetector import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import com.android.systemui.util.concurrency.ThreadFactory import dagger.Binds @@ -70,6 +71,11 @@ interface BiometricsModule { fun bindsSideFpsOverlayViewBinder(viewBinder: SideFpsOverlayViewBinder): CoreStartable @Binds + @IntoMap + @ClassKey(AlternateBouncerViewBinder::class) + fun bindAlternateBouncerViewBinder(viewBinder: AlternateBouncerViewBinder): CoreStartable + + @Binds @SysUISingleton fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt index 710667438299..2797b7b80ef9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt @@ -38,19 +38,14 @@ constructor( alternateBouncerInteractor: AlternateBouncerInteractor, systemUIDialogManager: SystemUIDialogManager, ) : UdfpsTouchOverlayViewModel { - private val showingUdfpsAffordance: Flow<Boolean> = + override val shouldHandleTouches: Flow<Boolean> = combine( deviceEntryIconViewModel.deviceEntryViewAlpha, alternateBouncerInteractor.isVisible, - ) { deviceEntryViewAlpha, alternateBouncerVisible -> - deviceEntryViewAlpha > ALLOW_TOUCH_ALPHA_THRESHOLD || alternateBouncerVisible - } - override val shouldHandleTouches: Flow<Boolean> = - combine( - showingUdfpsAffordance, - systemUIDialogManager.hideAffordancesRequest, - ) { showingUdfpsAffordance, dialogRequestingHideAffordances -> - showingUdfpsAffordance && !dialogRequestingHideAffordances + systemUIDialogManager.hideAffordancesRequest + ) { deviceEntryViewAlpha, alternateBouncerVisible, hideAffordancesRequest -> + (deviceEntryViewAlpha > ALLOW_TOUCH_ALPHA_THRESHOLD && !hideAffordancesRequest) || + alternateBouncerVisible } companion object { 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 000f03a8c6ec..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,12 +57,15 @@ 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 val isVisible: Flow<Boolean> = bouncerRepository.alternateBouncerVisible private val alternateBouncerUiAvailableFromSource: HashSet<String> = HashSet() - private val alternateBouncerSupported: StateFlow<Boolean> = + val alternateBouncerSupported: StateFlow<Boolean> = if (DeviceEntryUdfpsRefactor.isEnabled) { fingerprintPropertyRepository.sensorType .map { sensorType -> sensorType.isUdfps() || sensorType.isPowerButton() } @@ -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/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt index e298154159b2..17059097e053 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt @@ -20,9 +20,14 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.qs.tileimpl.QSTileViewImpl +import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch -object QSLongPressEffectViewBinder { +class QSLongPressEffectViewBinder { + + private var handle: DisposableHandle? = null + val isBound: Boolean + get() = handle != null fun bind( tile: QSTileViewImpl, @@ -30,33 +35,40 @@ object QSLongPressEffectViewBinder { ) { if (effect == null) return - tile.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - effect.scope = this + handle = + tile.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + effect.scope = this - launch { - effect.effectProgress.collect { progress -> - progress?.let { - if (it == 0f) { - tile.bringToFront() + launch { + effect.effectProgress.collect { progress -> + progress?.let { + if (it == 0f) { + tile.bringToFront() + } + tile.updateLongPressEffectProperties(it) } - tile.updateLongPressEffectProperties(it) } } - } - launch { - effect.actionType.collect { action -> - action?.let { - when (it) { - QSLongPressEffect.ActionType.CLICK -> tile.performClick() - QSLongPressEffect.ActionType.LONG_PRESS -> tile.performLongClick() + launch { + effect.actionType.collect { action -> + action?.let { + when (it) { + QSLongPressEffect.ActionType.CLICK -> tile.performClick() + QSLongPressEffect.ActionType.LONG_PRESS -> + tile.performLongClick() + } + effect.clearActionType() } - effect.clearActionType() } } } } - } + } + + fun dispose() { + handle?.dispose() + handle = null } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index d9d747015abd..fa845c7cf784 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -40,7 +40,6 @@ import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor -import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint @@ -52,7 +51,6 @@ import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscree import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.view.layout.KeyguardBlueprintCommandListener -import com.android.systemui.keyguard.ui.viewmodel.AodAlphaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel @@ -84,7 +82,6 @@ constructor( private val keyguardRootViewModel: KeyguardRootViewModel, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, private val notificationShadeWindowView: NotificationShadeWindowView, - private val featureFlags: FeatureFlagsClassic, private val indicationController: KeyguardIndicationController, private val screenOffAnimationController: ScreenOffAnimationController, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, @@ -101,13 +98,13 @@ constructor( private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, private val vibratorHelper: VibratorHelper, private val falsingManager: FalsingManager, - private val aodAlphaViewModel: AodAlphaViewModel, private val keyguardClockViewModel: KeyguardClockViewModel, private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val lockscreenContentViewModel: LockscreenContentViewModel, private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>, private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder, private val clockInteractor: KeyguardClockInteractor, + private val keyguardViewMediator: KeyguardViewMediator, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -209,6 +206,7 @@ constructor( deviceEntryHapticsInteractor, vibratorHelper, falsingManager, + keyguardViewMediator, ) } 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/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt index c749818a05e9..a861a8782ef9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt @@ -45,9 +45,14 @@ object AlternateBouncerUdfpsViewBinder { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.accessibilityDelegateHint.collect { hint -> - view.accessibilityHintType = hint + view.alpha = 0f + launch { + viewModel.accessibilityDelegateHint.collect { hint -> + view.accessibilityHintType = hint + } } + + launch { viewModel.alpha.collect { view.alpha = it } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt index 3a2781c81f7c..4cb342bec657 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt @@ -16,13 +16,19 @@ package com.android.systemui.keyguard.ui.binder -import android.view.View +import android.graphics.PixelFormat +import android.view.Gravity +import android.view.LayoutInflater import android.view.ViewGroup +import android.view.WindowManager import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.classifier.Classifier +import com.android.systemui.CoreStartable +import com.android.systemui.biometrics.Utils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay @@ -30,24 +36,98 @@ import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccess import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel +import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.scrim.ScrimView import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch /** - * Binds the alternate bouncer view to its view-model. + * When necessary, adds the alternate bouncer window above most other windows (including the + * notification shade, system UI dialogs) but below the UDFPS touch overlay and SideFPS indicator. + * Also binds the alternate bouncer view to its view-model. * - * For devices that support UDFPS, this includes a UDFPS view. + * For devices that support UDFPS, this view includes a UDFPS view. */ -@ExperimentalCoroutinesApi -object AlternateBouncerViewBinder { +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class AlternateBouncerViewBinder +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val alternateBouncerWindowViewModel: Lazy<AlternateBouncerWindowViewModel>, + private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>, + private val windowManager: Lazy<WindowManager>, + private val layoutInflater: Lazy<LayoutInflater>, +) : CoreStartable { + private val layoutParams: WindowManager.LayoutParams + get() = + WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, + Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS, + PixelFormat.TRANSLUCENT + ) + .apply { + title = "AlternateBouncerView" + fitInsetsTypes = 0 // overrides default, avoiding status bars during layout + gravity = Gravity.TOP or Gravity.LEFT + layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + privateFlags = + WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION + } + private var alternateBouncerView: ConstraintLayout? = null + + override fun start() { + if (!DeviceEntryUdfpsRefactor.isEnabled) { + return + } + applicationScope.launch { + alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect { + addAlternateBouncerWindowView -> + if (addAlternateBouncerWindowView) { + addViewToWindowManager() + val scrim = + alternateBouncerView!!.requireViewById(R.id.alternate_bouncer_scrim) + as ScrimView + scrim.viewAlpha = 0f + bind(alternateBouncerView!!, alternateBouncerDependencies.get()) + } else { + removeViewFromWindowManager() + alternateBouncerDependencies.get().viewModel.hideAlternateBouncer() + } + } + } + } + + private fun removeViewFromWindowManager() { + if (alternateBouncerView == null || !alternateBouncerView!!.isAttachedToWindow) { + return + } + + windowManager.get().removeView(alternateBouncerView) + } + + private fun addViewToWindowManager() { + if (alternateBouncerView?.isAttachedToWindow == true) { + return + } + + alternateBouncerView = + layoutInflater.get().inflate(R.layout.alternate_bouncer, null, false) + as ConstraintLayout + + windowManager.get().addView(alternateBouncerView, layoutParams) + } /** Binds the view to the view-model, continuing to update the former based on the latter. */ - @JvmStatic fun bind( view: ConstraintLayout, alternateBouncerDependencies: AlternateBouncerDependencies, @@ -71,27 +151,20 @@ object AlternateBouncerViewBinder { val viewModel = alternateBouncerDependencies.viewModel val swipeUpAnywhereGestureHandler = alternateBouncerDependencies.swipeUpAnywhereGestureHandler - val falsingManager = alternateBouncerDependencies.falsingManager val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector view.repeatWhenAttached { alternateBouncerViewContainer -> repeatOnLifecycle(Lifecycle.State.STARTED) { - scrim.viewAlpha = 0f - launch { viewModel.registerForDismissGestures.collect { registerForDismissGestures -> if (registerForDismissGestures) { swipeUpAnywhereGestureHandler.addOnGestureDetectedCallback(swipeTag) { _ -> - if ( - !falsingManager.isFalseTouch(Classifier.ALTERNATE_BOUNCER_SWIPE) - ) { - viewModel.showPrimaryBouncer() - } + alternateBouncerDependencies.powerInteractor.onUserTouch() + viewModel.showPrimaryBouncer() } tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ -> - if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { - viewModel.showPrimaryBouncer() - } + alternateBouncerDependencies.powerInteractor.onUserTouch() + viewModel.showPrimaryBouncer() } } else { swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback(swipeTag) @@ -100,20 +173,7 @@ object AlternateBouncerViewBinder { } } - launch { - viewModel.scrimAlpha.collect { - val wasVisible = alternateBouncerViewContainer.visibility == View.VISIBLE - alternateBouncerViewContainer.visibility = - if (it < .1f) View.INVISIBLE else View.VISIBLE - scrim.viewAlpha = it - if ( - wasVisible && alternateBouncerViewContainer.visibility == View.INVISIBLE - ) { - // view is no longer visible - viewModel.hideAlternateBouncer() - } - } - } + launch { viewModel.scrimAlpha.collect { scrim.viewAlpha = it } } launch { viewModel.scrimColor.collect { scrim.tint = it } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 0ed42ef75026..4d9354dd1572 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -44,6 +44,7 @@ import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState @@ -97,6 +98,7 @@ object KeyguardRootViewBinder { deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, vibratorHelper: VibratorHelper?, falsingManager: FalsingManager?, + keyguardViewMediator: KeyguardViewMediator?, ): DisposableHandle { var onLayoutChangeListener: OnLayoutChange? = null val childViews = mutableMapOf<Int, View>() @@ -298,8 +300,12 @@ object KeyguardRootViewBinder { } TransitionState.CANCELED -> jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) - TransitionState.FINISHED -> + TransitionState.FINISHED -> { + if (MigrateClocksToBlueprint.isEnabled) { + keyguardViewMediator?.maybeHandlePendingLock() + } jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) + } TransitionState.RUNNING -> Unit } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 14ab17f9641b..cbf52efed22f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -387,6 +387,7 @@ constructor( null, // device entry haptics not required preview mode null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode + null, // keyguard view mediator is not required for preview mode ) } rootView.addView( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt index 065c20ac52e8..b432417802c9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerDependencies.kt @@ -18,7 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler -import com.android.systemui.plugins.FalsingManager +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.gesture.TapGestureDetector import dagger.Lazy import javax.inject.Inject @@ -30,11 +30,11 @@ class AlternateBouncerDependencies @Inject constructor( val viewModel: AlternateBouncerViewModel, - val falsingManager: FalsingManager, val swipeUpAnywhereGestureHandler: SwipeUpAnywhereGestureHandler, val tapGestureDetector: TapGestureDetector, val udfpsIconViewModel: AlternateBouncerUdfpsIconViewModel, val udfpsAccessibilityOverlayViewModel: Lazy<AlternateBouncerUdfpsAccessibilityOverlayViewModel>, val messageAreaViewModel: AlternateBouncerMessageAreaViewModel, + val powerInteractor: PowerInteractor, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt index ce4511237e40..ded680c8baa3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.ui.view.DeviceEntryIconView +import com.android.systemui.shared.recents.utilities.Utilities.clamp import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -44,8 +45,13 @@ constructor( deviceEntryBackgroundViewModel: DeviceEntryBackgroundViewModel, fingerprintPropertyInteractor: FingerprintPropertyInteractor, udfpsOverlayInteractor: UdfpsOverlayInteractor, + alternateBouncerViewModel: AlternateBouncerViewModel, ) { private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported + val alpha: Flow<Float> = + alternateBouncerViewModel.transitionToAlternateBouncerProgress.map { + clamp(it * 2f, 0f, 1f) + } /** * UDFPS icon location in pixels for the current display and screen resolution, in natural diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt index 10a9e3bba7f0..06a0c7291f92 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt @@ -71,7 +71,7 @@ constructor( ) /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */ - private val transitionToAlternateBouncerProgress = + val transitionToAlternateBouncerProgress = merge(fromAlternateBouncerTransition, toAlternateBouncerTransition) val forcePluginOpen: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt new file mode 100644 index 000000000000..7814576eff01 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +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 javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@ExperimentalCoroutinesApi +class AlternateBouncerWindowViewModel +@Inject +constructor( + alternateBouncerInteractor: AlternateBouncerInteractor, + keyguardTransitionInteractor: KeyguardTransitionInteractor, +) { + private val deviceSupportsAlternateBouncer: Flow<Boolean> = + alternateBouncerInteractor.alternateBouncerSupported + private val isTransitioningToOrFromOrShowingAlternateBouncer: Flow<Boolean> = + keyguardTransitionInteractor.transitions + .map { + it.to == KeyguardState.ALTERNATE_BOUNCER || + (it.from == KeyguardState.ALTERNATE_BOUNCER && + it.transitionState != TransitionState.FINISHED) + } + .distinctUntilChanged() + + val alternateBouncerWindowRequired: Flow<Boolean> = + deviceSupportsAlternateBouncer.flatMapLatest { deviceSupportsAlternateBouncer -> + if (deviceSupportsAlternateBouncer) { + isTransitioningToOrFromOrShowingAlternateBouncer + } else { + flowOf(false) + } + } +} 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/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 0a880293ca76..9e313797674d 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -23,6 +23,7 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; +import static android.appwidget.flags.Flags.drawDataParcel; import static android.appwidget.flags.Flags.generatedPreviews; import static android.content.Intent.ACTION_BOOT_COMPLETED; import static android.content.Intent.ACTION_PACKAGE_ADDED; @@ -71,6 +72,7 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.graphics.drawable.Icon; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; @@ -111,6 +113,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.wm.shell.bubbles.Bubbles; +import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; @@ -1452,13 +1456,54 @@ public class PeopleSpaceWidgetManager implements Dumpable { if (DEBUG) { Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier()); } + if (!drawDataParcel() || (!Build.IS_USERDEBUG && !Build.IS_ENG)) { + updateGeneratedPreviewForUserInternal(provider, user, + new RemoteViews(mContext.getPackageName(), + R.layout.people_space_placeholder_layout)); + } else { + mBgExecutor.execute(updateGeneratedPreviewFromDrawInstructionsForUser(provider, user)); + } + } + + private void updateGeneratedPreviewForUserInternal(@NonNull final ComponentName provider, + @NonNull final UserHandle user, @NonNull final RemoteViews rv) { boolean success = mAppWidgetManager.setWidgetPreview( provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, - new RemoteViews(mContext.getPackageName(), - R.layout.people_space_placeholder_layout)); + rv); if (DEBUG && !success) { Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier()); } mUpdatedPreviews.put(user.getIdentifier(), success); } + + private Runnable updateGeneratedPreviewFromDrawInstructionsForUser( + @NonNull final ComponentName provider, @NonNull final UserHandle user) { + return () -> { + if (DEBUG) { + Log.d(TAG, "Parsing People Space widget preview from binary for user " + + user.getIdentifier()); + } + if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier()) + || !mUserManager.isUserUnlocked(user)) { + // Conditions may have changed given this is called from background thread + return; + } + try (InputStream is = mContext.getResources().openRawResource(R.raw.widget) + ) { + final byte[] preview = new byte[(int) is.available()]; + final int result = is.read(preview); + if (DEBUG && result == -1) { + Log.d(TAG, "Failed parsing previews from binary for user " + + user.getIdentifier()); + } + updateGeneratedPreviewForUserInternal(provider, user, new RemoteViews( + new RemoteViews.DrawInstructions.Builder( + Collections.singletonList(preview)).build())); + } catch (IOException e) { + if (DEBUG) { + Log.e(TAG, "Failed to generate preview for people widget", e); + } + } + }; + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index e1c543f8f025..2360f27fc115 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -181,11 +181,14 @@ open class QSTileViewImpl @JvmOverloads constructor( /** Visuo-haptic long-press effects */ private var longPressEffect: QSLongPressEffect? = null + private val longPressEffectViewBinder = QSLongPressEffectViewBinder() private var initialLongPressProperties: QSLongPressProperties? = null private var finalLongPressProperties: QSLongPressProperties? = null private val colorEvaluator = ArgbEvaluator.getInstance() val hasLongPressEffect: Boolean get() = longPressEffect != null + @VisibleForTesting val isLongPressEffectBound: Boolean + get() = longPressEffectViewBinder.isBound init { val typedValue = TypedValue() @@ -616,11 +619,14 @@ open class QSTileViewImpl @JvmOverloads constructor( // set the valid long-press effect as the touch listener showRippleEffect = false setOnTouchListener(longPressEffect) - QSLongPressEffectViewBinder.bind(this, longPressEffect) + if (!longPressEffectViewBinder.isBound) { + longPressEffectViewBinder.bind(this, longPressEffect) + } } else { // Long-press effects might have been enabled before but the new state does not // handle a long-press. In this case, we go back to the behaviour of a regular tile // and clean-up the resources + longPressEffectViewBinder.dispose() showRippleEffect = isClickable setOnTouchListener(null) longPressEffect = null diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index e8e629ca907d..59da8f1d5841 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -51,8 +51,6 @@ import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; -import com.android.systemui.keyguard.ui.binder.AlternateBouncerViewBinder; -import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies; import com.android.systemui.res.R; import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener; import com.android.systemui.statusbar.DragDownHelper; @@ -72,11 +70,8 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; -import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.time.SystemClock; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.Optional; import java.util.function.Consumer; @@ -186,8 +181,6 @@ public class NotificationShadeWindowViewController implements Dumpable { QuickSettingsController quickSettingsController, PrimaryBouncerInteractor primaryBouncerInteractor, AlternateBouncerInteractor alternateBouncerInteractor, - Lazy<JavaAdapter> javaAdapter, - Lazy<AlternateBouncerDependencies> alternateBouncerDependencies, BouncerViewBinder bouncerViewBinder) { mLockscreenShadeTransitionController = transitionController; mFalsingCollector = falsingCollector; @@ -224,23 +217,6 @@ public class NotificationShadeWindowViewController implements Dumpable { mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView); bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container)); - if (DeviceEntryUdfpsRefactor.isEnabled()) { - AlternateBouncerViewBinder.bind( - mView.findViewById(R.id.alternate_bouncer), - alternateBouncerDependencies.get() - ); - javaAdapter.get().alwaysCollectFlow( - alternateBouncerDependencies.get().getViewModel() - .getForcePluginOpen(), - forcePluginOpen -> - mNotificationShadeWindowController.setForcePluginOpen( - forcePluginOpen, - alternateBouncerDependencies.get() - .getViewModel() - ) - ); - } - collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), mLockscreenToDreamingTransition); collectFlow( diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index 9963f813b884..5b2377f7f610 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -96,6 +96,7 @@ interface ShadeViewController { val shadeHeadsUpTracker: ShadeHeadsUpTracker /** Returns the ShadeFoldAnimator. */ + @Deprecated("This interface is deprecated in Scene Container") val shadeFoldAnimator: ShadeFoldAnimator companion object { @@ -142,9 +143,10 @@ interface ShadeHeadsUpTracker { } /** Handles the lifecycle of the shade's animation that happens when folding a foldable. */ +@Deprecated("This interface should not be used in scene container.") interface ShadeFoldAnimator { /** Updates the views to the initial state for the fold to AOD animation. */ - fun prepareFoldToAodAnimation() + @Deprecated("Not used when migrateClocksToBlueprint enabled") fun prepareFoldToAodAnimation() /** * Starts fold to AOD animation. @@ -153,21 +155,24 @@ interface ShadeFoldAnimator { * @param endAction invoked when the animation finishes, also if it was cancelled. * @param cancelAction invoked when the animation is cancelled, before endAction. */ + @Deprecated("Not used when migrateClocksToBlueprint enabled") fun startFoldToAodAnimation(startAction: Runnable, endAction: Runnable, cancelAction: Runnable) /** Cancels fold to AOD transition and resets view state. */ - fun cancelFoldToAodAnimation() + @Deprecated("Not used when migrateClocksToBlueprint enabled") fun cancelFoldToAodAnimation() /** Returns the main view of the shade. */ - val view: ViewGroup? + @Deprecated("Not used in Scene Container") val view: ViewGroup? } /** * An interface that provides the current state of the notification panel and related views, which * is needed to calculate [KeyguardStatusBarView]'s state in [KeyguardStatusBarViewController]. */ +@Deprecated("This interface should not be used in scene container.") interface ShadeViewStateProvider { /** Returns the expanded height of the panel view. */ + @Deprecated("deprecated by migrate_keyguard_status_bar_view flag") val panelViewExpandedHeight: Float /** @@ -176,8 +181,9 @@ interface ShadeViewStateProvider { * TODO(b/138786270): If HeadsUpAppearanceController was injectable, we could inject it into * [KeyguardStatusBarViewController] and remove this method. */ - fun shouldHeadsUpBeVisible(): Boolean + @Deprecated("deprecated in Flexiglass.") fun shouldHeadsUpBeVisible(): Boolean /** Return the fraction of the shade that's expanded, when in lockscreen. */ + @Deprecated("deprecated by migrate_keyguard_status_bar_view flag") val lockscreenShadeDragProgress: Float } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index fb528386018b..bef26d9958ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -205,7 +205,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private boolean mAnimateNextTopPaddingChange; private int mBottomPadding; @VisibleForTesting - int mBottomInset = 0; + // mImeInset=0 when IME is hidden + int mImeInset = 0; private float mQsExpansionFraction; private final int mSplitShadeMinContentHeight; private String mLastUpdateSidePaddingDumpString; @@ -396,7 +397,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public WindowInsets onProgress(WindowInsets windowInsets, List<WindowInsetsAnimation> list) { - updateBottomInset(windowInsets); + updateImeInset(windowInsets); return windowInsets; } @@ -1792,8 +1793,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable + ((!isExpanded() && isPinnedHeadsUp(v)) ? mHeadsUpInset : getTopPadding()); } - private void updateBottomInset(WindowInsets windowInsets) { - mBottomInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; + private void updateImeInset(WindowInsets windowInsets) { + mImeInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; if (mForcedScroll != null) { updateForcedScroll(); @@ -1814,7 +1815,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (!mIsInsetAnimationRunning) { // update bottom inset e.g. after rotation - updateBottomInset(insets); + updateImeInset(insets); } return insets; } @@ -2218,9 +2219,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int getImeInset() { // The NotificationStackScrollLayout does not extend all the way to the bottom of the - // display. Therefore, subtract that space from the mBottomInset, in order to only include + // display. Therefore, subtract that space from the mImeInset, in order to only include // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. - return Math.max(0, mBottomInset + return Math.max(0, mImeInset - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1])); } 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/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt index 098d51e94fc7..81c8d5039d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -21,15 +21,12 @@ import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager import android.provider.Settings -import androidx.annotation.VisibleForTesting import androidx.core.view.OneShotPreDrawListener -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle import com.android.internal.util.LatencyTracker +import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.shade.ShadeFoldAnimator import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.LightRevealScrim @@ -40,9 +37,6 @@ import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationSt import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.settings.GlobalSettings import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import java.util.function.Consumer import javax.inject.Inject @@ -69,7 +63,6 @@ constructor( private var isFoldHandled = true private var alwaysOnEnabled: Boolean = false - private var isDozing: Boolean = false private var isScrimOpaque: Boolean = false private var pendingScrimReadyCallback: Runnable? = null @@ -97,11 +90,6 @@ constructor( deviceStateManager.registerCallback(mainExecutor, FoldListener()) wakefulnessLifecycle.addObserver(this) - - // TODO(b/254878364): remove this call to NPVC.getView() - getShadeFoldAnimator().view?.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) } - } } /** Returns true if we should run fold to AOD animation */ @@ -158,7 +146,8 @@ constructor( } else { pendingScrimReadyCallback = onReady } - } else if (isFolded && !isFoldHandled && alwaysOnEnabled && isDozing) { + } else if (isFolded && !isFoldHandled && alwaysOnEnabled && + keyguardInteractor.get().isDozing.value) { setAnimationState(playing = true) getShadeFoldAnimator().prepareFoldToAodAnimation() @@ -166,8 +155,10 @@ constructor( // but we should wait for the initial animation preparations to be drawn // (setting initial alpha/translation) // TODO(b/254878364): remove this call to NPVC.getView() - getShadeFoldAnimator().view?.let { - OneShotPreDrawListener.add(it, onReady) + if (!migrateClocksToBlueprint()) { + getShadeFoldAnimator().view?.let { + OneShotPreDrawListener.add(it, onReady) + } } } else { // No animation, call ready callback immediately @@ -233,11 +224,6 @@ constructor( statusListeners.remove(listener) } - @VisibleForTesting - internal suspend fun listenForDozing(scope: CoroutineScope): Job { - return scope.launch { keyguardInteractor.get().isDozing.collect { isDozing = it } } - } - interface FoldAodAnimationStatus { fun onFoldToAodAnimationChanged() } 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/AlternateBouncerWindowViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt new file mode 100644 index 000000000000..143c4dacb6be --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +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.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@ExperimentalCoroutinesApi +@RunWith(JUnit4::class) +@SmallTest +class AlternateBouncerWindowViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository } + private val transitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val underTest by lazy { kosmos.alternateBouncerWindowViewModel } + + @Test + fun alternateBouncerTransition_alternateBouncerWindowRequiredTrue() = + testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) + val alternateBouncerWindowRequired by + collectLastValue(underTest.alternateBouncerWindowRequired) + fingerprintPropertyRepository.supportsUdfps() + transitionRepository.sendTransitionSteps( + listOf( + stepFromAlternateBouncer(0f, TransitionState.STARTED), + stepFromAlternateBouncer(.4f), + stepFromAlternateBouncer(.6f), + stepFromAlternateBouncer(1f), + ), + testScope, + ) + assertThat(alternateBouncerWindowRequired).isTrue() + } + + @Test + fun deviceEntryUdfpsFlagDisabled_alternateBouncerWindowRequiredFalse() = + testScope.runTest { + mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) + val alternateBouncerWindowRequired by + collectLastValue(underTest.alternateBouncerWindowRequired) + fingerprintPropertyRepository.supportsUdfps() + transitionRepository.sendTransitionSteps( + listOf( + stepFromAlternateBouncer(0f, TransitionState.STARTED), + stepFromAlternateBouncer(.4f), + stepFromAlternateBouncer(.6f), + stepFromAlternateBouncer(1f), + ), + testScope, + ) + assertThat(alternateBouncerWindowRequired).isFalse() + } + + @Test + fun lockscreenTransition_alternateBouncerWindowRequiredFalse() = + testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) + val alternateBouncerWindowRequired by + collectLastValue(underTest.alternateBouncerWindowRequired) + fingerprintPropertyRepository.supportsUdfps() + transitionRepository.sendTransitionSteps( + listOf( + stepFromDozingToLockscreen(0f, TransitionState.STARTED), + stepFromDozingToLockscreen(.4f), + stepFromDozingToLockscreen(.6f), + stepFromDozingToLockscreen(1f), + ), + testScope, + ) + assertThat(alternateBouncerWindowRequired).isFalse() + } + + @Test + fun rearFps_alternateBouncerWindowRequiredFalse() = + testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) + val alternateBouncerWindowRequired by + collectLastValue(underTest.alternateBouncerWindowRequired) + fingerprintPropertyRepository.supportsRearFps() + transitionRepository.sendTransitionSteps( + listOf( + stepFromAlternateBouncer(0f, TransitionState.STARTED), + stepFromAlternateBouncer(.4f), + stepFromAlternateBouncer(.6f), + stepFromAlternateBouncer(1f), + ), + testScope, + ) + assertThat(alternateBouncerWindowRequired).isFalse() + } + + private fun stepFromAlternateBouncer( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return step( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = state, + ) + } + + private fun stepFromDozingToLockscreen( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return step( + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = state, + ) + } + + private fun step( + from: KeyguardState, + to: KeyguardState, + value: Float, + transitionState: TransitionState + ): TransitionStep { + return TransitionStep( + from = from, + to = to, + value = value, + transitionState = transitionState, + ownerName = "AlternateBouncerViewModelTest" + ) + } +} 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/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java index db0c0bcfa8f5..890e1e011f22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -138,6 +138,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @SmallTest @@ -1576,6 +1578,7 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { @Test public void testUpdateGeneratedPreview_flagDisabled() { mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any()); } @@ -1583,6 +1586,7 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { @Test public void testUpdateGeneratedPreview_userLocked() { mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false); mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); @@ -1592,6 +1596,7 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { @Test public void testUpdateGeneratedPreview_userUnlocked() { mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); @@ -1602,6 +1607,7 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { @Test public void testUpdateGeneratedPreview_doesNotSetTwice() { mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); @@ -1610,6 +1616,54 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any()); } + @Test + public void testUpdateGeneratedPreviewWithDataParcel_userLocked() throws InterruptedException { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + assertThat(waitForBackgroundJob()).isTrue(); + verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any()); + } + + @Test + public void testUpdateGeneratedPreviewWithDataParcel_userUnlocked() + throws InterruptedException { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); + when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + assertThat(waitForBackgroundJob()).isTrue(); + verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any()); + } + + @Test + public void testUpdateGeneratedPreviewWithDataParcel_doesNotSetTwice() + throws InterruptedException { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); + when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + assertThat(waitForBackgroundJob()).isTrue(); + verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any()); + } + + private boolean waitForBackgroundJob() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + mFakeExecutor.execute(latch::countDown); + mFakeExecutor.runAllReady(); + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + return latch.await(30000, TimeUnit.MILLISECONDS); + + } + private void setFinalField(String fieldName, int value) { try { Field field = NotificationManager.Policy.class.getDeclaredField(fieldName); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt index 04e214ac7a04..2b1ac915f430 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt @@ -410,6 +410,36 @@ class QSTileViewImplTest : SysuiTestCase() { assertThat(tileView.hasLongPressEffect).isTrue() } + @Test + @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) + fun onStateChange_fromLongPress_to_noLongPress_unBoundsTile() { + // GIVEN a state that no longer handles long-press + val state = QSTile.State() + state.handlesLongClick = false + + // WHEN the state changes + tileView.changeState(state) + + // THEN the view binder no longer binds the view to the long-press effect + assertThat(tileView.isLongPressEffectBound).isFalse() + } + + @Test + @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) + fun onStateChange_fromNoLongPress_to_longPress_bindsTile() { + // GIVEN that the tile has changed to a state that does not handle long-press + val state = QSTile.State() + state.handlesLongClick = false + tileView.changeState(state) + + // WHEN the state changes back to handling long-press + state.handlesLongClick = true + tileView.changeState(state) + + // THEN the view is bounded to the long-press effect + assertThat(tileView.isLongPressEffectBound).isTrue() + } + class FakeTileView( context: Context, collapsed: Boolean diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 88b239a77433..b6996131ea09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade -import org.mockito.Mockito.`when` as whenever import android.content.Context import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -45,7 +44,6 @@ import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.DragDownHelper @@ -66,12 +64,10 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.window.StatusBarWindowStateController import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat -import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -89,6 +85,8 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import java.util.Optional +import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -206,8 +204,6 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { quickSettingsController, primaryBouncerInteractor, alternateBouncerInteractor, - { mock(JavaAdapter::class.java) }, - { mock(AlternateBouncerDependencies::class.java) }, mock(BouncerViewBinder::class.java) ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 59fe813cf18e..2ecca2e5c4ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -36,7 +36,6 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.DragDownHelper @@ -56,7 +55,6 @@ import com.android.systemui.statusbar.phone.DozeServiceHost import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.window.StatusBarWindowStateController import com.android.systemui.unfold.UnfoldTransitionProgressProvider -import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -195,9 +193,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { quickSettingsController, primaryBouncerInteractor, alternateBouncerInteractor, - { Mockito.mock(JavaAdapter::class.java) }, - { Mockito.mock(AlternateBouncerDependencies::class.java) }, - mock() + mock(), ) controller.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt index 6f16d65e14f9..811e9bfb3332 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt @@ -5,9 +5,10 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos import com.android.systemui.res.R import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder @@ -39,7 +40,6 @@ import org.mockito.junit.MockitoJUnit class NotificationTransitionAnimatorControllerTest : SysuiTestCase() { @Mock lateinit var notificationListContainer: NotificationListContainer @Mock lateinit var headsUpManager: HeadsUpManager - @Mock lateinit var jankMonitor: InteractionJankMonitor @Mock lateinit var onFinishAnimationCallback: Runnable private lateinit var notificationTestHelper: NotificationTestHelper @@ -67,7 +67,7 @@ class NotificationTransitionAnimatorControllerTest : SysuiTestCase() { notificationListContainer, headsUpManager, notification, - jankMonitor, + Kosmos().interactionJankMonitor, onFinishAnimationCallback ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 13df09134b23..abb9432425bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -931,14 +931,14 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Test public void testWindowInsetAnimationProgress_updatesBottomInset() { - int bottomImeInset = 100; + int imeInset = 100; WindowInsets windowInsets = new WindowInsets.Builder() - .setInsets(ime(), Insets.of(0, 0, 0, bottomImeInset)).build(); + .setInsets(ime(), Insets.of(0, 0, 0, imeInset)).build(); ArrayList<WindowInsetsAnimation> windowInsetsAnimations = new ArrayList<>(); mStackScrollerInternal .dispatchWindowInsetsAnimationProgress(windowInsets, windowInsetsAnimations); - assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset); + assertEquals(imeInset, mStackScrollerInternal.mImeInset); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt index 10aab96e6036..cb7d2764bb49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt @@ -133,47 +133,34 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { @Test fun onFolded_aodDisabled_doesNotLogLatency() = runBlocking(IMMEDIATE) { - val job = underTest.listenForDozing(this) keyguardRepository.setIsDozing(true) setAodEnabled(enabled = false) - yield() - fold() simulateScreenTurningOn() verifyNoMoreInteractions(latencyTracker) - - job.cancel() } @Test fun onFolded_aodEnabled_logsLatency() = runBlocking(IMMEDIATE) { - val job = underTest.listenForDozing(this) keyguardRepository.setIsDozing(true) setAodEnabled(enabled = true) - yield() - fold() simulateScreenTurningOn() verify(latencyTracker).onActionStart(any()) verify(latencyTracker).onActionEnd(any()) - - job.cancel() } @Test fun onFolded_onScreenTurningOnInvokedTwice_doesNotLogLatency() = runBlocking(IMMEDIATE) { - val job = underTest.listenForDozing(this) keyguardRepository.setIsDozing(true) setAodEnabled(enabled = true) - yield() - fold() simulateScreenTurningOn() reset(latencyTracker) @@ -183,19 +170,14 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { verify(latencyTracker, never()).onActionStart(any()) verify(latencyTracker, never()).onActionEnd(any()) - - job.cancel() } @Test fun onFolded_onScreenTurningOnWithoutDozingThenWithDozing_doesNotLogLatency() = runBlocking(IMMEDIATE) { - val job = underTest.listenForDozing(this) keyguardRepository.setIsDozing(false) setAodEnabled(enabled = true) - yield() - fold() simulateScreenTurningOn() reset(latencyTracker) @@ -208,19 +190,14 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { verify(latencyTracker, never()).onActionStart(any()) verify(latencyTracker, never()).onActionEnd(any()) - - job.cancel() } @Test fun onFolded_animationCancelled_doesNotLogLatency() = runBlocking(IMMEDIATE) { - val job = underTest.listenForDozing(this) keyguardRepository.setIsDozing(true) setAodEnabled(enabled = true) - yield() - fold() underTest.onScreenTurningOn({}) // The body of onScreenTurningOn is executed on fakeExecutor, @@ -230,8 +207,6 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { verify(latencyTracker).onActionStart(any()) verify(latencyTracker).onActionCancel(any()) - - job.cancel() } private fun simulateScreenTurningOn() { 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/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelKosmos.kt new file mode 100644 index 000000000000..92cfbef987f6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelKosmos.kt @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.alternateBouncerWindowViewModel by Fixture { + AlternateBouncerWindowViewModel( + alternateBouncerInteractor = alternateBouncerInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + ) +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 880a68776055..a57138f9de72 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4815,7 +4815,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub uid, packageName); final boolean ecmEnabled = mContext.getResources().getBoolean( com.android.internal.R.bool.config_enhancedConfirmationModeEnabled); - return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; + return !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED + || mode == AppOpsManager.MODE_DEFAULT; } catch (Exception e) { // Fallback in case if app ops is not available in testing. return false; diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 16fe0077d6ff..cfeb5f4e2469 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -107,6 +107,7 @@ import android.util.IntArray; import android.util.Log; import android.util.LongSparseArray; import android.util.Pair; +import android.util.SizeF; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -163,6 +164,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.LongSupplier; +import java.util.stream.Collectors; class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider, OnCrossProfileWidgetProvidersChangeListener { @@ -176,6 +178,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private static final String STATE_FILENAME = "appwidgets.xml"; + private static final String KEY_SIZES = "sizes"; + private static final String SIZE_SEPARATOR = ","; + private static final int MIN_UPDATE_PERIOD = DEBUG ? 0 : 30 * 60 * 1000; // 30 minutes private static final int TAG_UNDEFINED = -1; @@ -2710,6 +2715,13 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku out.attributeIntHex(null, "max_height", (maxHeight > 0) ? maxHeight : 0); out.attributeIntHex(null, "host_category", widget.options.getInt( AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)); + List<SizeF> sizes = widget.options.getParcelableArrayList( + AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF.class); + if (sizes != null) { + String sizeStr = sizes.stream().map(SizeF::toString) + .collect(Collectors.joining(SIZE_SEPARATOR)); + out.attribute(null, KEY_SIZES, sizeStr); + } if (saveRestoreCompleted) { boolean restoreCompleted = widget.options.getBoolean( AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED); @@ -2741,6 +2753,17 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (maxHeight != -1) { options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxHeight); } + String sizesStr = parser.getAttributeValue(null, KEY_SIZES); + if (sizesStr != null) { + try { + ArrayList<SizeF> sizes = Arrays.stream(sizesStr.split(SIZE_SEPARATOR)) + .map(SizeF::parseSizeF) + .collect(Collectors.toCollection(ArrayList::new)); + options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizes); + } catch (NumberFormatException e) { + Slog.e(TAG, "Error parsing widget sizes", e); + } + } int category = parser.getAttributeIntHex(null, "host_category", AppWidgetProviderInfo.WIDGET_CATEGORY_UNKNOWN); if (category != AppWidgetProviderInfo.WIDGET_CATEGORY_UNKNOWN) { diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index d006cf6d703a..993a1d5494a2 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -2383,6 +2383,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_FAILURE); } mPresentationStatsEventLogger.logAndEndEvent(); + mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); mFillResponseEventLogger.logAndEndEvent(); } notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED, @@ -5400,6 +5401,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } // Log the existing FillResponse event. mFillResponseEventLogger.maybeSetAvailableCount(0); + mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); mFillResponseEventLogger.logAndEndEvent(); mService.resetLastResponse(); diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java new file mode 100644 index 000000000000..0a4148535451 --- /dev/null +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -0,0 +1,567 @@ +/* + * 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 f4f6c13e74e4..712162b2d3b5 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -20,10 +20,15 @@ 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; @@ -37,10 +42,13 @@ 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; @@ -56,6 +64,7 @@ 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; @@ -70,6 +79,7 @@ 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; @@ -81,6 +91,7 @@ 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; @@ -107,8 +118,7 @@ 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.CompanionAppBinder; -import com.android.server.companion.presence.DevicePresenceProcessor; +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.transport.CompanionTransportManager; @@ -121,7 +131,10 @@ 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") @@ -133,6 +146,10 @@ 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; @@ -148,11 +165,10 @@ public class CompanionDeviceManagerService extends SystemService { private final AssociationRequestsProcessor mAssociationRequestsProcessor; private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final BackupRestoreProcessor mBackupRestoreProcessor; - private final DevicePresenceProcessor mDevicePresenceMonitor; - private final CompanionAppBinder mCompanionAppController; + private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final CompanionApplicationController mCompanionAppController; private final CompanionTransportManager mTransportManager; private final DisassociationProcessor mDisassociationProcessor; - private final InactiveAssociationsRemovalService mInactiveAssociationsRemovalService; private final CrossDeviceSyncController mCrossDeviceSyncController; public CompanionDeviceManagerService(Context context) { @@ -169,7 +185,7 @@ public class CompanionDeviceManagerService extends SystemService { mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); final AssociationDiskStore associationDiskStore = new AssociationDiskStore(); - mAssociationStore = new AssociationStore(context, userManager, associationDiskStore); + mAssociationStore = new AssociationStore(userManager, associationDiskStore); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); mObservableUuidStore = new ObservableUuidStore(); @@ -180,11 +196,11 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); - mCompanionAppController = new CompanionAppBinder( - context, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); + mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager, + mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); - mDevicePresenceMonitor = new DevicePresenceProcessor(context, - mCompanionAppController, userManager, mAssociationStore, mObservableUuidStore, + mCompanionAppController = new CompanionApplicationController( + context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor, mPowerManagerInternal); mTransportManager = new CompanionTransportManager(context, mAssociationStore); @@ -193,9 +209,6 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor, mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager); - mInactiveAssociationsRemovalService = new InactiveAssociationsRemovalService( - mAssociationStore, mDisassociationProcessor); - mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); @@ -289,6 +302,181 @@ 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) { @@ -334,8 +522,27 @@ public class CompanionDeviceManagerService extends SystemService { mBackupRestoreProcessor.restorePendingAssociations(userId, packageName); } + // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { - mInactiveAssociationsRemovalService.removeIdleSelfManagedAssociations(); + 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); + } } public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { @@ -472,15 +679,24 @@ 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); - mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress); + final AssociationInfo association = + getAssociationWithCallerChecks(userId, packageName, deviceMacAddress); + mDisassociationProcessor.disassociate(association.getId()); } @Override public void disassociate(int associationId) { - mDisassociationProcessor.disassociate(associationId); + Slog.i(TAG, "disassociate() associationId=" + associationId); + + final AssociationInfo association = + getAssociationWithCallerChecks(associationId); + mDisassociationProcessor.disassociate(association.getId()); } @Override @@ -542,25 +758,21 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage, - int userId) throws RemoteException { - legacyStartObservingDevicePresence_enforcePermission(); - - mDevicePresenceMonitor.startObservingDevicePresence(userId, callingPackage, - deviceAddress); + public void registerDevicePresenceListenerService(String deviceAddress, + String callingPackage, int userId) throws RemoteException { + registerDevicePresenceListenerService_enforcePermission(); + // TODO: take the userId into account. + registerDevicePresenceListenerActive(callingPackage, deviceAddress, true); } @Override - @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage, - int userId) throws RemoteException { - legacyStopObservingDevicePresence_enforcePermission(); - - mDevicePresenceMonitor.stopObservingDevicePresence(userId, callingPackage, - deviceAddress); + public void unregisterDevicePresenceListenerService(String deviceAddress, + String callingPackage, int userId) throws RemoteException { + unregisterDevicePresenceListenerService_enforcePermission(); + // TODO: take the userId into account. + registerDevicePresenceListenerActive(callingPackage, deviceAddress, false); } @Override @@ -568,8 +780,7 @@ public class CompanionDeviceManagerService extends SystemService { public void startObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { startObservingDevicePresence_enforcePermission(); - - mDevicePresenceMonitor.startObservingDevicePresence(request, packageName, userId); + registerDevicePresenceListener(request, packageName, userId, /* active */ true); } @Override @@ -577,8 +788,80 @@ 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(); - mDevicePresenceMonitor.stopObservingDevicePresence(request, packageName, userId); + 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); + } } @Override @@ -591,7 +874,8 @@ public class CompanionDeviceManagerService extends SystemService { @Override public boolean isPermissionTransferUserConsented(String packageName, int userId, int associationId) { - return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId); + return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName, + userId, associationId); } @Override @@ -607,7 +891,8 @@ public class CompanionDeviceManagerService extends SystemService { ParcelFileDescriptor fd) { attachSystemDataTransport_enforcePermission(); - mTransportManager.attachSystemDataTransport(associationId, fd); + getAssociationWithCallerChecks(associationId); + mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd); } @Override @@ -615,56 +900,96 @@ public class CompanionDeviceManagerService extends SystemService { public void detachSystemDataTransport(String packageName, int userId, int associationId) { detachSystemDataTransport_enforcePermission(); - mTransportManager.detachSystemDataTransport(associationId); - } - - @Override - @EnforcePermission(MANAGE_COMPANION_DEVICES) - public void enableSecureTransport(boolean enabled) { - enableSecureTransport_enforcePermission(); - - mTransportManager.enableSecureTransport(enabled); + getAssociationWithCallerChecks(associationId); + mTransportManager.detachSystemDataTransport(packageName, userId, associationId); } @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(REQUEST_COMPANION_SELF_MANAGED) - public void notifySelfManagedDeviceAppeared(int associationId) { - notifySelfManagedDeviceAppeared_enforcePermission(); + @EnforcePermission(MANAGE_COMPANION_DEVICES) + public void enableSecureTransport(boolean enabled) { + enableSecureTransport_enforcePermission(); + mTransportManager.enableSecureTransport(enabled); + } - mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, true); + @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); + } } @Override - @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) - public void notifySelfManagedDeviceDisappeared(int associationId) { - notifySelfManagedDeviceDisappeared_enforcePermission(); + 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); - mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, false); + 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); + } } @Override @@ -672,6 +997,66 @@ public class CompanionDeviceManagerService extends SystemService { 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); + } + } + @Override @EnforcePermission(ASSOCIATE_COMPANION_DEVICES) public void createAssociation(String packageName, String macAddress, int userId, @@ -685,8 +1070,7 @@ public class CompanionDeviceManagerService extends SystemService { } final MacAddress macAddressObj = MacAddress.fromString(macAddress); - mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, - null, null, null, false, null, null); + createNewAssociation(userId, packageName, macAddressObj, null, null, false); } private void checkCanCallNotificationApi(String callingPackage, int userId) { @@ -715,7 +1099,9 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void setAssociationTag(int associationId, String tag) { - mAssociationRequestsProcessor.setAssociationTag(associationId, tag); + AssociationInfo association = getAssociationWithCallerChecks(associationId); + association = (new AssociationInfo.Builder(association)).setTag(tag).build(); + mAssociationStore.updateAssociation(association); } @Override @@ -760,6 +1146,14 @@ public class CompanionDeviceManagerService extends SystemService { } } + 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 */ @@ -775,6 +1169,8 @@ 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)) { @@ -884,6 +1280,29 @@ 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) { @@ -896,7 +1315,7 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void onPackageModified(@NonNull String packageName) { + public void onPackageModified(String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } @@ -906,12 +1325,28 @@ 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(); + } + + @Override public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, @CrossDeviceSyncControllerCallback.Type int type) { if (CompanionDeviceConfig.isEnabled( diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index e3b4c95a7dab..cdf832f8c788 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -28,6 +28,11 @@ import java.util.Collection; */ public interface CompanionDeviceManagerServiceInternal { /** + * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations + */ + void removeInactiveSelfManagedAssociations(); + + /** * Registers a callback from an InCallService / ConnectionService to CDM to process sync * requests and perform call control actions. */ diff --git a/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java index c01c3195e04d..5abdb42b34fc 100644 --- a/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion.presence; +package com.android.server.companion; import static android.content.Context.BIND_ALMOST_PERCEPTIBLE; import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE; @@ -33,42 +33,36 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; -import android.util.Slog; +import android.util.Log; 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") -public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - - /** 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 CompanionServiceConnector serviceConnector); - } - +class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { private static final String TAG = "CDM_CompanionServiceConnector"; + private static final boolean DEBUG = false; /* 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; + + /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector} */ + interface Listener { + void onBindingDied(@UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionDeviceServiceConnector serviceConnector); + } + + private final @UserIdInt int mUserId; + private final @NonNull ComponentName mComponentName; // 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. - @Nullable - private Listener mListener; + private @Nullable Listener mListener; + private boolean mIsPrimary; /** * Create a CompanionDeviceServiceConnector instance. @@ -85,16 +79,16 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD * IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the * service importance level should be higher than 125. */ - static CompanionServiceConnector newInstance(@NonNull Context context, + static CompanionDeviceServiceConnector 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 CompanionServiceConnector( + return new CompanionDeviceServiceConnector( context, userId, componentName, bindingFlags, isPrimary); } - private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId, + private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) { super(context, buildIntent(componentName), bindingFlags, userId, null); mUserId = userId; @@ -139,7 +133,6 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD return mIsPrimary; } - @NonNull ComponentName getComponentName() { return mComponentName; } @@ -147,15 +140,17 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD @Override protected void onServiceConnectionStatusChanged( @NonNull ICompanionDeviceService service, boolean isConnected) { - Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString() - + " connected=" + isConnected); + if (DEBUG) { + Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString() + + " connected=" + isConnected); + } } @Override public void binderDied() { super.binderDied(); - Slog.d(TAG, "binderDied() " + mComponentName.toShortString()); + if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString()); // Handle primary process being killed if (mListener != null) { @@ -177,8 +172,7 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD * within system_server and thus tends to get heavily congested) */ @Override - @NonNull - protected Handler getJobHandler() { + protected @NonNull Handler getJobHandler() { return getServiceThread().getThreadHandler(); } @@ -188,14 +182,12 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD return -1; } - @NonNull - private static Intent buildIntent(@NonNull ComponentName componentName) { + private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) { return new Intent(CompanionDeviceService.SERVICE_INTERFACE) .setComponent(componentName); } - @NonNull - private static ServiceThread getServiceThread() { + private static @NonNull ServiceThread getServiceThread() { if (sServiceThread == null) { synchronized (CompanionDeviceManagerService.class) { if (sServiceThread == null) { @@ -214,6 +206,5 @@ public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionD * <p> * Do NOT reference directly, use {@link #getServiceThread()} method instead. */ - @Nullable - private static volatile ServiceThread sServiceThread; + private static volatile @Nullable ServiceThread sServiceThread; } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index a78938400a1e..a7a73cb6bddb 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,6 +18,8 @@ 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; @@ -36,7 +38,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.DevicePresenceProcessor; +import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.transport.CompanionTransportManager; @@ -49,7 +51,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final CompanionDeviceManagerService mService; private final DisassociationProcessor mDisassociationProcessor; private final AssociationStore mAssociationStore; - private final DevicePresenceProcessor mDevicePresenceProcessor; + private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; private final CompanionTransportManager mTransportManager; private final SystemDataTransferProcessor mSystemDataTransferProcessor; @@ -58,7 +60,7 @@ class CompanionDeviceShellCommand extends ShellCommand { CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStore associationStore, - DevicePresenceProcessor devicePresenceProcessor, + CompanionDevicePresenceMonitor devicePresenceMonitor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor, @@ -66,7 +68,7 @@ class CompanionDeviceShellCommand extends ShellCommand { DisassociationProcessor disassociationProcessor) { mService = service; mAssociationStore = associationStore; - mDevicePresenceProcessor = devicePresenceProcessor; + mDevicePresenceMonitor = devicePresenceMonitor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; @@ -83,7 +85,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) { associationId = getNextIntArgRequired(); int event = getNextIntArgRequired(); - mDevicePresenceProcessor.simulateDeviceEvent(associationId, event); + mDevicePresenceMonitor.simulateDeviceEvent(associationId, event); return 0; } @@ -95,7 +97,7 @@ class CompanionDeviceShellCommand extends ShellCommand { ObservableUuid observableUuid = new ObservableUuid( userId, ParcelUuid.fromString(uuid), packageName, System.currentTimeMillis()); - mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event); + mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event); return 0; } @@ -122,9 +124,8 @@ class CompanionDeviceShellCommand extends ShellCommand { String address = getNextArgRequired(); String deviceProfile = getNextArg(); final MacAddress macAddress = MacAddress.fromString(address); - mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - deviceProfile, deviceProfile, /* associatedDevice */ null, false, - /* callback */ null, /* resultReceiver */ null); + mService.createNewAssociation(userId, packageName, macAddress, + /* displayName= */ deviceProfile, deviceProfile, false); } break; @@ -133,13 +134,8 @@ class CompanionDeviceShellCommand extends ShellCommand { final String packageName = getNextArgRequired(); final String address = getNextArgRequired(); final AssociationInfo association = - mAssociationStore.getFirstAssociationByAddress(userId, packageName, - address); - if (association == null) { - out.println("Association doesn't exist."); - } else { - mDisassociationProcessor.disassociate(association.getId()); - } + mService.getAssociationWithCallerChecks(userId, packageName, address); + mDisassociationProcessor.disassociate(association.getId()); } break; @@ -148,7 +144,9 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - mDisassociationProcessor.disassociate(association.getId()); + if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { + mDisassociationProcessor.disassociate(association.getId()); + } } } break; @@ -159,12 +157,12 @@ class CompanionDeviceShellCommand extends ShellCommand { case "simulate-device-appeared": associationId = getNextIntArgRequired(); - mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0); + mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0); break; case "simulate-device-disappeared": associationId = getNextIntArgRequired(); - mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1); + mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); break; case "get-backup-payload": { @@ -412,9 +410,10 @@ 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(" refresh-cache"); + pw.println(" clear-association-memory-cache"); pw.println(" Clear the in-memory association cache and reload all association "); - pw.println(" information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); + pw.println(" 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 a18776e67200..a02d9f912bcd 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -145,8 +145,7 @@ 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, @@ -213,8 +212,7 @@ public class AssociationRequestsProcessor { // 2b.4. Send the PendingIntent back to the app. try { callback.onAssociationPending(pendingIntent); - } catch (RemoteException ignore) { - } + } catch (RemoteException ignore) { } } /** @@ -254,8 +252,7 @@ public class AssociationRequestsProcessor { // forward it back to the application via the callback. try { callback.onFailure(e.getMessage()); - } catch (RemoteException ignore) { - } + } catch (RemoteException ignore) { } return; } @@ -325,8 +322,7 @@ public class AssociationRequestsProcessor { * Enable system data sync. */ public void enableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( - associationId); + AssociationInfo association = mAssociationStore.getAssociationById(associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build(); mAssociationStore.updateAssociation(updated); @@ -336,23 +332,12 @@ public class AssociationRequestsProcessor { * Disable system data sync. */ public void disableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( - associationId); + AssociationInfo association = mAssociationStore.getAssociationById(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) { @@ -411,14 +396,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 ae2b70852a35..edebb55233d0 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -18,7 +18,6 @@ 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; @@ -27,7 +26,6 @@ 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; @@ -59,22 +57,21 @@ 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. @@ -103,30 +100,25 @@ 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 Context mContext; - private final UserManager mUserManager; - private final AssociationDiskStore mDiskStore; + private final Object mLock = new Object(); + private final ExecutorService mExecutor; - private final Object mLock = new Object(); @GuardedBy("mLock") private boolean mPersisted = false; @GuardedBy("mLock") @@ -140,9 +132,10 @@ public class AssociationStore { private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners = new RemoteCallbackList<>(); - public AssociationStore(Context context, UserManager userManager, - AssociationDiskStore diskStore) { - mContext = context; + private final UserManager mUserManager; + private final AssociationDiskStore mDiskStore; + + public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) { mUserManager = userManager; mDiskStore = diskStore; mExecutor = Executors.newSingleThreadExecutor(); @@ -209,7 +202,7 @@ public class AssociationStore { synchronized (mLock) { if (mIdToAssociationMap.containsKey(id)) { - Slog.e(TAG, "Association id=[" + id + "] already exists."); + Slog.e(TAG, "Association with id=[" + id + "] already exists."); return; } @@ -456,26 +449,6 @@ 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 20de1210dd9d..ec8977918c56 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -33,19 +33,18 @@ import android.os.Binder; 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.CompanionAppBinder; -import com.android.server.companion.presence.DevicePresenceProcessor; +import com.android.server.companion.presence.CompanionDevicePresenceMonitor; import com.android.server.companion.transport.CompanionTransportManager; /** - * This class responsible for disassociation. + * A class response for Association removal. */ @SuppressLint("LongLogTag") public class DisassociationProcessor { private static final String TAG = "CDM_DisassociationProcessor"; - @NonNull private final Context mContext; @NonNull @@ -53,11 +52,11 @@ public class DisassociationProcessor { @NonNull private final PackageManagerInternal mPackageManagerInternal; @NonNull - private final DevicePresenceProcessor mDevicePresenceMonitor; + private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull - private final CompanionAppBinder mCompanionAppController; + private final CompanionApplicationController mCompanionAppController; @NonNull private final CompanionTransportManager mTransportManager; private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; @@ -67,8 +66,8 @@ public class DisassociationProcessor { @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, - @NonNull DevicePresenceProcessor devicePresenceMonitor, - @NonNull CompanionAppBinder applicationController, + @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, + @NonNull CompanionApplicationController applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager) { mContext = context; @@ -90,7 +89,11 @@ public class DisassociationProcessor { public void disassociate(int id) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); - final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); + final AssociationInfo association = mAssociationStore.getAssociationById(id); + if (association == null) { + Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist."); + return; + } final int userId = association.getUserId(); final String packageName = association.getPackageName(); @@ -115,12 +118,12 @@ public class DisassociationProcessor { return; } - // Detach transport if exists - mTransportManager.detachSystemDataTransport(id); - // Association cleanup. - mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); mAssociationStore.removeAssociation(association.getId()); + mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); + + // Detach transport if exists + mTransportManager.detachSystemDataTransport(packageName, userId, id); // 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. @@ -144,24 +147,6 @@ public class DisassociationProcessor { } } - /** - * @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(() -> { @@ -178,7 +163,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."); } } 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 b52904aa5301..f28731548dcc 100644 --- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -22,14 +22,15 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; -import android.companion.AssociationInfo; import android.content.ComponentName; import android.content.Context; -import android.os.SystemProperties; import android.util.Slog; +import com.android.server.LocalServices; +import com.android.server.companion.CompanionDeviceManagerServiceInternal; + /** - * A Job Service responsible for clean up self-managed associations if it's idle for 90 days. + * A Job Service responsible for clean up idle self-managed associations. * * 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} @@ -40,25 +41,14 @@ public class InactiveAssociationsRemovalService extends JobService { private static final String JOB_NAMESPACE = "companion"; private static final int JOB_ID = 1; private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1); - 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 final AssociationStore mAssociationStore; - private final DisassociationProcessor mDisassociationProcessor; - - public InactiveAssociationsRemovalService(AssociationStore associationStore, - DisassociationProcessor disassociationProcessor) { - mAssociationStore = associationStore; - mDisassociationProcessor = disassociationProcessor; - } @Override public boolean onStartJob(final JobParameters params) { Slog.i(TAG, "Execute the Association Removal job"); - - removeIdleSelfManagedAssociations(); - + // 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; } @@ -87,29 +77,4 @@ public class InactiveAssociationsRemovalService extends JobService { .build(); jobScheduler.schedule(job); } - - /** - * Remove idle self-managed associations. - */ - public void removeIdleSelfManagedAssociations() { - 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); - } - } } 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 9069689ee5eb..c5ca0bf7e9c5 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -31,6 +31,7 @@ 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; @@ -55,6 +56,7 @@ 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; @@ -118,10 +120,28 @@ 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(int associationId) { - mAssociationStore.getAssociationWithCallerChecks(associationId); + public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId, + int associationId) { + resolveAssociation(packageName, userId, associationId); PermissionSyncRequest request = getPermissionSyncRequest(associationId); if (request == null) { @@ -147,8 +167,7 @@ public class SystemDataTransferProcessor { return null; } - final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( - associationId); + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId + "] associationId [" + associationId + "]"); @@ -188,7 +207,7 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - mAssociationStore.getAssociationWithCallerChecks(associationId); + final AssociationInfo association = resolveAssociation(packageName, userId, associationId); // Check if the request has been consented by the user. PermissionSyncRequest request = getPermissionSyncRequest(associationId); @@ -220,20 +239,24 @@ public class SystemDataTransferProcessor { * Enable perm sync for the association */ public void enablePermissionsSync(int associationId) { - int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(true); - mSystemDataTransferRequestStore.writeRequest(userId, request); + Binder.withCleanCallingIdentity(() -> { + int userId = mAssociationStore.getAssociationById(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) { - int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(false); - mSystemDataTransferRequestStore.writeRequest(userId, request); + Binder.withCleanCallingIdentity(() -> { + int userId = mAssociationStore.getAssociationById(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(false); + mSystemDataTransferRequestStore.writeRequest(userId, request); + }); } /** @@ -241,17 +264,18 @@ public class SystemDataTransferProcessor { */ @Nullable public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - 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 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; + } } - } - 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 9c37881499bd..c89ce11c169d 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.DevicePresenceProcessor.DEBUG; +import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.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 2d345c48a8eb..cb363a7c9d7f 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.DevicePresenceProcessor.DEBUG; +import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.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 deleted file mode 100644 index 4ba4e2ce6899..000000000000 --- a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java +++ /dev/null @@ -1,392 +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 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.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.CompanionDeviceManagerService; -import com.android.server.companion.association.AssociationStore; -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 CompanionAppBinder} (to be - * utilized by {@link CompanionDeviceManagerService}): - * <ul> - * <li> {@link #bindCompanionApplication(int, String, boolean, CompanionServiceConnector.Listener)} - * <li> {@link #unbindCompanionApplication(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 AssociationStore mAssociationStore; - @NonNull - private final ObservableUuidStore mObservableUuidStore; - @NonNull - private final CompanionServicesRegister mCompanionServicesRegister; - - private final PowerManagerInternal mPowerManagerInternal; - - @NonNull - @GuardedBy("mBoundCompanionApplications") - private final AndroidPackageMap<List<CompanionServiceConnector>> - mBoundCompanionApplications; - @NonNull - @GuardedBy("mScheduledForRebindingCompanionApplications") - private final AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; - - public CompanionAppBinder(@NonNull Context context, - @NonNull AssociationStore associationStore, - @NonNull ObservableUuidStore observableUuidStore, - @NonNull PowerManagerInternal powerManagerInternal) { - mContext = context; - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mPowerManagerInternal = powerManagerInternal; - mCompanionServicesRegister = new CompanionServicesRegister(); - mBoundCompanionApplications = new AndroidPackageMap<>(); - mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); - } - - /** - * On package changed. - */ - public 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, 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.containsValueForPackage(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.setValueForPackage(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 unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { - Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]..."); - - final List<CompanionServiceConnector> serviceConnectors; - - synchronized (mBoundCompanionApplications) { - serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(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.containsValueForPackage(userId, packageName); - } - } - - /** - * Remove bound apps for package. - */ - public void removePackage(int userId, String packageName) { - synchronized (mBoundCompanionApplications) { - mBoundCompanionApplications.removePackage(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.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 CompanionServiceConnector serviceConnector) { - // Re-mark the application is bound. - if (serviceConnector.isPrimary()) { - synchronized (mBoundCompanionApplications) { - if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - List<CompanionServiceConnector> serviceConnectors = - Collections.singletonList(serviceConnector); - mBoundCompanionApplications.setValueForPackage(userId, packageName, - serviceConnectors); - } - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(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.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mBoundCompanionApplications.dump(out); - } - } - - out.append(" Companion Applications Scheduled For Rebinding: "); - synchronized (mScheduledForRebindingCompanionApplications) { - if (mScheduledForRebindingCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mScheduledForRebindingCompanionApplications.dump(out); - } - } - } - - @Nullable - CompanionServiceConnector getPrimaryServiceConnector( - @UserIdInt int userId, @NonNull String packageName) { - final List<CompanionServiceConnector> connectors; - synchronized (mBoundCompanionApplications) { - connectors = mBoundCompanionApplications.getValueForPackage(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); - } - } - - /** - * 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/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java new file mode 100644 index 000000000000..7a1a83f53315 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java @@ -0,0 +1,620 @@ +/* + * 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/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java deleted file mode 100644 index 2a933a8340c6..000000000000 --- a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java +++ /dev/null @@ -1,1042 +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.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.unbindCompanionApplication(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.bindCompanionApplication( - 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.unbindCompanionApplication(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.unbindCompanionApplication(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 fa0f6bd92acb..db15da2922cf 100644 --- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java +++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java @@ -300,18 +300,4 @@ 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 697ef87b5a12..793fb7ff74b1 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -46,6 +46,7 @@ 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; @@ -136,17 +137,11 @@ public class CompanionTransportManager { } } - /** - * Attach transport. - */ - public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { - Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); - - mAssociationStore.getAssociationWithCallerChecks(associationId); - + public void attachSystemDataTransport(String packageName, int userId, int associationId, + ParcelFileDescriptor fd) { synchronized (mTransports) { if (mTransports.contains(associationId)) { - detachSystemDataTransport(associationId); + detachSystemDataTransport(packageName, userId, associationId); } // TODO: Implement new API to pass a PSK @@ -154,18 +149,9 @@ public class CompanionTransportManager { notifyOnTransportsChanged(); } - - Slog.i(TAG, "Transport attached."); } - /** - * Detach transport. - */ - public void detachSystemDataTransport(int associationId) { - Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]..."); - - mAssociationStore.getAssociationWithCallerChecks(associationId); - + public void detachSystemDataTransport(String packageName, int userId, int associationId) { synchronized (mTransports) { final Transport transport = mTransports.removeReturnOld(associationId); if (transport == null) { @@ -175,8 +161,6 @@ public class CompanionTransportManager { transport.stop(); notifyOnTransportsChanged(); } - - Slog.i(TAG, "Transport detached."); } private void notifyOnTransportsChanged() { @@ -323,7 +307,8 @@ public class CompanionTransportManager { int associationId = transport.mAssociationId; AssociationInfo association = mAssociationStore.getAssociationById(associationId); if (association != null) { - detachSystemDataTransport( + detachSystemDataTransport(association.getPackageName(), + association.getUserId(), association.getId()); } } diff --git a/services/companion/java/com/android/server/companion/utils/PackageUtils.java b/services/companion/java/com/android/server/companion/utils/PackageUtils.java index 81dc36ddcff1..254d28b1b2c2 100644 --- a/services/companion/java/com/android/server/companion/utils/PackageUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PackageUtils.java @@ -241,7 +241,7 @@ public final class PackageUtils { final int mode = context.getSystemService(AppOpsManager.class).noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, /* attributionTag= */ null, /* message= */ null); - return mode == AppOpsManager.MODE_ALLOWED; + return mode == AppOpsManager.MODE_ALLOWED || mode == AppOpsManager.MODE_DEFAULT; } } } 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 d7e766eed209..2cf1f462a7d1 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -39,6 +39,7 @@ 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; @@ -207,7 +208,7 @@ public final class PermissionsUtils { /** * Require the caller to hold necessary permission to observe device presence by UUID. */ - public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) { + public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) { if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) != PERMISSION_GRANTED) { throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " @@ -234,6 +235,23 @@ 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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 0f9517460ee9..649b9efc3282 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -4622,6 +4622,7 @@ public class AudioService extends IAudioService.Stub case AudioSystem.MODE_CALL_SCREENING: case AudioSystem.MODE_CALL_REDIRECT: case AudioSystem.MODE_COMMUNICATION_REDIRECT: + case AudioSystem.MODE_RINGTONE: break; default: // no-op is enough for all other values 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/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index a5f241f4d68e..49a8553dd2cd 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -11,6 +11,14 @@ flag { } flag { + name: "resolution_backup_restore" + namespace: "display_manager" + description: "Backup/Restore support for High Resolution setting" + bug: "321821289" + is_fixed_read_only: true +} + +flag { name: "enable_connected_display_management" namespace: "display_manager" description: "Feature flag for Connected Display management" diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java index dd6433d98553..c7b60da2fc51 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java @@ -19,13 +19,11 @@ package com.android.server.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.content.Context; import android.content.pm.UserInfo; import android.os.Handler; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; -import com.android.internal.inputmethod.DirectBootAwareness; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; @@ -69,7 +67,7 @@ final class AdditionalSubtypeMapRepository { AdditionalSubtypeUtils.save(map, inputMethodMap, userId); } - static void initialize(@NonNull Handler handler, @NonNull Context context) { + static void initialize(@NonNull Handler handler) { final UserManagerInternal userManagerInternal = LocalServices.getService(UserManagerInternal.class); handler.post(() -> { @@ -81,16 +79,8 @@ final class AdditionalSubtypeMapRepository { handler.post(() -> { synchronized (ImfLock.class) { if (!sPerUserMap.contains(userId)) { - final AdditionalSubtypeMap additionalSubtypeMap = - AdditionalSubtypeUtils.load(userId); - sPerUserMap.put(userId, additionalSubtypeMap); - final InputMethodSettings settings = - InputMethodManagerService - .queryInputMethodServicesInternal(context, - userId, - additionalSubtypeMap, - DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(userId, settings); + sPerUserMap.put(userId, + AdditionalSubtypeUtils.load(userId)); } } }); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java index 99c7397ea85c..a100fe06c407 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java @@ -74,6 +74,7 @@ final class InputMethodBindingController { @GuardedBy("ImfLock.class") @Nullable private IInputMethodInvoker mCurMethod; @GuardedBy("ImfLock.class") private int mCurMethodUid = Process.INVALID_UID; @GuardedBy("ImfLock.class") @Nullable private IBinder mCurToken; + @GuardedBy("ImfLock.class") private int mCurSeq; @GuardedBy("ImfLock.class") private boolean mVisibleBound; @GuardedBy("ImfLock.class") private boolean mSupportsStylusHw; @GuardedBy("ImfLock.class") private boolean mSupportsConnectionlessStylusHw; @@ -119,14 +120,6 @@ final class InputMethodBindingController { } /** - * Interface used to abstract {@code InputMethodBindingController} instantiation. - */ - interface Creator { - - InputMethodBindingController create(); - } - - /** * Time that we last initiated a bind to the input method, to determine * if we should try to disconnect and reconnect to it. */ @@ -202,6 +195,27 @@ final class InputMethodBindingController { } /** + * The current binding sequence number, incremented every time there is + * a new bind performed. + */ + @GuardedBy("ImfLock.class") + int getSequenceNumber() { + return mCurSeq; + } + + /** + * Increase the current binding sequence number by one. + * Reset to 1 on overflow. + */ + @GuardedBy("ImfLock.class") + void advanceSequenceNumber() { + mCurSeq += 1; + if (mCurSeq <= 0) { + mCurSeq = 1; + } + } + + /** * If non-null, this is the input method service we are currently connected * to. */ @@ -421,11 +435,9 @@ final class InputMethodBindingController { mLastBindTime = SystemClock.uptimeMillis(); addFreshWindowToken(); - final UserData monitor = UserData.getOrCreate( - mService.getCurrentImeUserIdLocked()); return new InputBindResult( InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING, - null, null, null, mCurId, monitor.mSequence.getSequenceNumber(), false); + null, null, null, mCurId, mCurSeq, false); } Slog.w(InputMethodManagerService.TAG, diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c406e17deb7f..b595f0e71d16 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -284,12 +284,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final Context mContext; final Resources mRes; private final Handler mHandler; - + @NonNull @MultiUserUnawareField - @UserIdInt - @GuardedBy("ImfLock.class") - private int mCurrentUserId; - + private InputMethodSettings mSettings; @MultiUserUnawareField final SettingsObserver mSettingsObserver; final WindowManagerInternal mWindowManagerInternal; @@ -303,6 +300,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @MultiUserUnawareField private final InputMethodMenuController mMenuController; @MultiUserUnawareField + @NonNull private final InputMethodBindingController mBindingController; + @MultiUserUnawareField @NonNull private final AutofillSuggestionsController mAutofillController; @GuardedBy("ImfLock.class") @@ -463,14 +462,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable String getSelectedMethodIdLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getSelectedMethodId(); + return mBindingController.getSelectedMethodId(); } @GuardedBy("ImfLock.class") private void setSelectedMethodIdLocked(@Nullable String selectedMethodId) { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - userData.mBindingController.setSelectedMethodId(selectedMethodId); + mBindingController.setSelectedMethodId(selectedMethodId); } /** @@ -479,8 +476,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private int getSequenceNumberLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mSequence.getSequenceNumber(); + return mBindingController.getSequenceNumber(); } /** @@ -489,14 +485,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private void advanceSequenceNumberLocked() { - final UserData monitor = UserData.getOrCreate(mCurrentUserId); - monitor.mSequence.advanceSequenceNumber(); + mBindingController.advanceSequenceNumber(); } @GuardedBy("ImfLock.class") @Nullable InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) { - return InputMethodSettingsRepository.get(mCurrentUserId).getMethodMap().get(imeId); + return mSettings.getMethodMap().get(imeId); } /** @@ -550,8 +545,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable private String getCurIdLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurId(); + return mBindingController.getCurId(); } /** @@ -575,8 +569,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private boolean hasConnectionLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.hasMainConnection(); + return mBindingController.hasMainConnection(); } /** @@ -599,8 +592,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable private Intent getCurIntentLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurIntent(); + return mBindingController.getCurIntent(); } /** @@ -610,8 +602,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable IBinder getCurTokenLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurToken(); + return mBindingController.getCurToken(); } /** @@ -652,8 +643,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable IInputMethodInvoker getCurMethodLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurMethod(); + return mBindingController.getCurMethod(); } /** @@ -661,8 +651,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private int getCurMethodUidLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getCurMethodUid(); + return mBindingController.getCurMethodUid(); } /** @@ -671,8 +660,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private long getLastBindTimeLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - return userData.mBindingController.getLastBindTime(); + return mBindingController.getLastBindTime(); } /** @@ -824,8 +812,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodManager.invalidateLocalStylusHandwritingAvailabilityCaches(); } else { boolean enabledChanged = false; - String newEnabled = InputMethodSettingsRepository.get(mCurrentUserId) - .getEnabledInputMethodsStr(); + String newEnabled = mSettings.getEnabledInputMethodsStr(); if (!mLastEnabled.equals(newEnabled)) { mLastEnabled = newEnabled; enabledChanged = true; @@ -857,11 +844,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // sender userId can be a real user ID or USER_ALL. final int senderUserId = pendingResult.getSendingUserId(); if (senderUserId != UserHandle.USER_ALL) { - synchronized (ImfLock.class) { - if (senderUserId != mCurrentUserId) { - // A background user is trying to hide the dialog. Ignore. - return; - } + if (senderUserId != mSettings.getUserId()) { + // A background user is trying to hide the dialog. Ignore. + return; } } mMenuController.hideInputMethodMenu(); @@ -885,14 +870,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!mSystemReady) { return; } - for (int userId : mUserManagerInternal.getUserIds()) { - final InputMethodSettings settings = queryInputMethodServicesInternal( - mContext, - userId, - AdditionalSubtypeMapRepository.get(userId), - DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(userId, settings); - } + mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(), + AdditionalSubtypeMapRepository.get(mSettings.getUserId()), + DirectBootAwareness.AUTO); postInputMethodSettingUpdatedLocked(true /* resetDefaultEnabledIme */); // If the locale is changed, needs to reset the default ime resetDefaultImeLocked(mContext); @@ -953,7 +933,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean isChangingPackagesOfCurrentUserLocked() { final int userId = getChangingUserId(); - final boolean retval = userId == mCurrentUserId; + final boolean retval = userId == mSettings.getUserId(); if (DEBUG) { if (!retval) { Slog.d(TAG, "--- ignore this call back from a background user: " + userId); @@ -968,10 +948,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!isChangingPackagesOfCurrentUserLocked()) { return false; } - final InputMethodSettings settings = - InputMethodSettingsRepository.get(mCurrentUserId); - String curInputMethodId = settings.getSelectedInputMethod(); - final List<InputMethodInfo> methodList = settings.getMethodList(); + String curInputMethodId = mSettings.getSelectedInputMethod(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { @@ -1088,10 +1066,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void onFinishPackageChangesInternal() { synchronized (ImfLock.class) { final int userId = getChangingUserId(); - final boolean isCurrentUser = (userId == mCurrentUserId); + final boolean isCurrentUser = (userId == mSettings.getUserId()); final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final InputMethodSettings settings; + if (isCurrentUser) { + settings = mSettings; + } else { + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); + } InputMethodInfo curIm = null; String curInputMethodId = settings.getSelectedInputMethod(); @@ -1135,17 +1119,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); } - if (isCurrentUser - && !(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { + + if (!isCurrentUser) { return; } - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - userId, newAdditionalSubtypeMap, DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(userId, newSettings); - if (!isCurrentUser) { + if (!(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) { return; } + mSettings = queryInputMethodServicesInternal(mContext, userId, + newAdditionalSubtypeMap, DirectBootAwareness.AUTO); postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); boolean changed = false; @@ -1306,20 +1289,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void onUnlockUser(@UserIdInt int userId) { synchronized (ImfLock.class) { + final int currentUserId = mSettings.getUserId(); if (DEBUG) { - Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + mCurrentUserId); + Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId); } - if (!mSystemReady) { + if (userId != currentUserId) { return; } - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(userId, newSettings); - if (mCurrentUserId == userId) { - // We need to rebuild IMEs. - postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); - updateInputMethodsFromSettingsLocked(true /* enabledChanged */); + if (!mSystemReady) { + return; } + mSettings = queryInputMethodServicesInternal(mContext, userId, + AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO); + // We need to rebuild IMEs. + postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); + updateInputMethodsFromSettingsLocked(true /* enabledChanged */); } } @@ -1383,32 +1367,33 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mShowOngoingImeSwitcherForPhones = false; - // InputMethodSettingsRepository should be initialized before buildInputMethodListLocked - InputMethodSettingsRepository.initialize(mHandler, mContext); - AdditionalSubtypeMapRepository.initialize(mHandler, mContext); + AdditionalSubtypeMapRepository.initialize(mHandler); - mCurrentUserId = mActivityManagerInternal.getCurrentUserId(); + final int userId = mActivityManagerInternal.getCurrentUserId(); - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); + // mSettings should be created before buildInputMethodListLocked + mSettings = InputMethodSettings.createEmptyMap(userId); mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(context, - settings.getMethodMap(), settings.getUserId()); + mSettings.getMethodMap(), userId); mHardwareKeyboardShortcutController = - new HardwareKeyboardShortcutController(settings.getMethodMap(), - settings.getUserId()); + new HardwareKeyboardShortcutController(mSettings.getMethodMap(), + mSettings.getUserId()); mMenuController = new InputMethodMenuController(this); + mBindingController = + bindingControllerForTesting != null + ? bindingControllerForTesting + : new InputMethodBindingController(this); mAutofillController = new AutofillSuggestionsController(this); + mVisibilityStateComputer = new ImeVisibilityStateComputer(this); mVisibilityApplier = new DefaultImeVisibilityApplier(this); + mClientController = new ClientController(mPackageManagerInternal); synchronized (ImfLock.class) { mClientController.addClientControllerCallback(c -> onClientRemoved(c)); mImeBindingState = ImeBindingState.newEmptyState(); - UserData.initialize(mHandler, - /* bindingControllerCreator= */ () -> bindingControllerForTesting != null - ? bindingControllerForTesting - : new InputMethodBindingController(this)); } mPreventImeStartupUnlessTextEditor = mRes.getBoolean( @@ -1427,7 +1412,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @UserIdInt int getCurrentImeUserIdLocked() { - return mCurrentUserId; + return mSettings.getUserId(); } private final class InkWindowInitializer implements Runnable { @@ -1463,13 +1448,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = getSelectedMethodIdLocked(); - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); if (selectedMethodId != null - && !settings.getMethodMap().get(selectedMethodId).isSystem()) { + && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( - context, settings.getEnabledInputMethodList()); + context, mSettings.getEnabledInputMethodList()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); return; @@ -1525,7 +1509,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub IInputMethodClientInvoker clientToBeReset) { if (DEBUG) { Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId - + " currentUserId=" + mCurrentUserId); + + " currentUserId=" + mSettings.getUserId()); } maybeInitImeNavbarConfigLocked(newUserId); @@ -1533,9 +1517,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); - mCurrentUserId = newUserId; - final String defaultImiId = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_INPUT_METHOD, null, newUserId); + mSettings = InputMethodSettings.createEmptyMap(newUserId); + final String defaultImiId = mSettings.getSelectedInputMethod(); if (DEBUG) { Slog.d(TAG, "Switching user stage 2/3. newUserId=" + newUserId @@ -1553,9 +1536,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // and user switch would not happen at that time. resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_USER); - final InputMethodSettings newSettings = InputMethodSettingsRepository.get(newUserId); + mSettings = queryInputMethodServicesInternal(mContext, newUserId, + AdditionalSubtypeMapRepository.get(newUserId), DirectBootAwareness.AUTO); postInputMethodSettingUpdatedLocked(initialUserSwitch /* resetDefaultEnabledIme */); - if (TextUtils.isEmpty(newSettings.getSelectedInputMethod())) { + if (TextUtils.isEmpty(mSettings.getSelectedInputMethod())) { // This is the first time of the user switch and // set the current ime to the proper one. resetDefaultImeLocked(mContext); @@ -1565,12 +1549,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (initialUserSwitch) { InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, newUserId), - newSettings.getEnabledInputMethodList()); + mSettings.getEnabledInputMethodList()); } if (DEBUG) { Slog.d(TAG, "Switching user stage 3/3. newUserId=" + newUserId - + " selectedIme=" + newSettings.getSelectedInputMethod()); + + " selectedIme=" + mSettings.getSelectedInputMethod()); } if (mIsInteractive && clientToBeReset != null) { @@ -1593,7 +1577,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!mSystemReady) { mSystemReady = true; - final int currentUserId = mCurrentUserId; + final int currentUserId = mSettings.getUserId(); mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); hideStatusBarIconLocked(); @@ -1614,7 +1598,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the "mImeDrawsImeNavBarResLazyInitFuture" field. synchronized (ImfLock.class) { mImeDrawsImeNavBarResLazyInitFuture = null; - if (currentUserId != mCurrentUserId) { + if (currentUserId != mSettings.getUserId()) { // This means that the current user is already switched to other user // before the background task is executed. In this scenario the relevant // field should already be initialized. @@ -1633,19 +1617,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub UserHandle.ALL, broadcastFilterForAllUsers, null, null, Context.RECEIVER_EXPORTED); - final String defaultImiId = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_INPUT_METHOD, null, currentUserId); + final String defaultImiId = mSettings.getSelectedInputMethod(); final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId); - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - currentUserId, AdditionalSubtypeMapRepository.get(currentUserId), + mSettings = queryInputMethodServicesInternal(mContext, currentUserId, + AdditionalSubtypeMapRepository.get(mSettings.getUserId()), DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(currentUserId, newSettings); postInputMethodSettingUpdatedLocked( !imeSelectedOnBoot /* resetDefaultEnabledIme */); updateFromSettingsLocked(true); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - newSettings.getEnabledInputMethodList()); + mSettings.getEnabledInputMethodList()); } } } @@ -1692,7 +1674,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1715,7 +1697,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mCurrentUserId, null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1743,14 +1725,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // Check if selected IME of current user supports handwriting. - if (userId == mCurrentUserId) { - final UserData userData = UserData.getOrCreate(userId); - final InputMethodBindingController bindingController = userData.mBindingController; - return bindingController.supportsStylusHandwriting() + if (userId == mSettings.getUserId()) { + return mBindingController.supportsStylusHandwriting() && (!connectionless - || bindingController.supportsConnectionlessStylusHandwriting()); + || mBindingController.supportsConnectionlessStylusHandwriting()); } - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. + //TODO(b/210039666): use cache. + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); final InputMethodInfo imi = settings.getMethodMap().get( settings.getSelectedInputMethod()); return imi != null && imi.supportsStylusHandwriting() @@ -1774,8 +1756,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { final InputMethodSettings settings; - if (directBootAwareness == DirectBootAwareness.AUTO) { - settings = InputMethodSettingsRepository.get(userId); + if (userId == mSettings.getUserId() + && directBootAwareness == DirectBootAwareness.AUTO) { + settings = mSettings; } else { final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); @@ -1793,8 +1776,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodInfo> getEnabledInputMethodListLocked(@UserIdInt int userId, int callingUid) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - final ArrayList<InputMethodInfo> methodList = settings.getEnabledInputMethodList(); + final ArrayList<InputMethodInfo> methodList; + final InputMethodSettings settings; + if (userId == mSettings.getUserId()) { + methodList = mSettings.getEnabledInputMethodList(); + settings = mSettings; + } else { + settings = queryMethodMapForUserLocked(userId); + methodList = settings.getEnabledInputMethodList(); + } // filter caller's access to input methods methodList.removeIf(imi -> !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)); @@ -1852,7 +1842,22 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + if (userId == mSettings.getUserId()) { + final InputMethodInfo imi; + String selectedMethodId = getSelectedMethodIdLocked(); + if (imiId == null && selectedMethodId != null) { + imi = mSettings.getMethodMap().get(selectedMethodId); + } else { + imi = mSettings.getMethodMap().get(imiId); + } + if (imi == null || !canCallerAccessInputMethod( + imi.getPackageName(), callingUid, userId, mSettings)) { + return Collections.emptyList(); + } + return mSettings.getEnabledInputMethodSubtypeList( + imi, allowsImplicitlyEnabledSubtypes); + } + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); final InputMethodInfo imi = settings.getMethodMap().get(imiId); if (imi == null) { return Collections.emptyList(); @@ -2032,7 +2037,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean restarting = !initial; final Binder startInputToken = new Binder(); - final StartInputInfo info = new StartInputInfo(mCurrentUserId, + final StartInputInfo info = new StartInputInfo(mSettings.getUserId(), getCurTokenLocked(), mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting, UserHandle.getUserId(mCurClient.mUid), @@ -2046,9 +2051,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // same-user scenarios. // That said ignoring cross-user scenario will never affect IMEs that do not have // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case. - if (mCurrentUserId == UserHandle.getUserId( + if (mSettings.getUserId() == UserHandle.getUserId( mCurClient.mUid)) { - mPackageManagerInternal.grantImplicitAccess(mCurrentUserId, + mPackageManagerInternal.grantImplicitAccess(mSettings.getUserId(), null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()), mCurClient.mUid, true /* direct */); } @@ -2071,14 +2076,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } String curId = getCurIdLocked(); - final InputMethodInfo curInputMethodInfo = InputMethodSettingsRepository.get(mCurrentUserId) - .getMethodMap().get(curId); + final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId); final boolean suppressesSpellChecker = curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker(); final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions = createAccessibilityInputMethodSessions(mCurClient.mAccessibilitySessions); - final UserData userData = UserData.getOrCreate(mCurrentUserId); - if (userData.mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) { + if (mBindingController.supportsStylusHandwriting() && hasSupportedStylusLocked()) { mHwController.setInkWindowInitializer(new InkWindowInitializer()); } return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION, @@ -2193,14 +2196,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurEditorInfo = editorInfo; // If configured, we want to avoid starting up the IME if it is not supposed to be showing - final UserData userData = UserData.getOrCreate(mCurrentUserId); if (shouldPreventImeStartupLocked(selectedMethodId, startInputFlags, unverifiedTargetSdkVersion)) { if (DEBUG) { Slog.d(TAG, "Avoiding IME startup and unbinding current input method."); } invalidateAutofillSessionLocked(); - userData.mBindingController.unbindCurrentMethod(); + mBindingController.unbindCurrentMethod(); return InputBindResult.NO_EDITOR; } @@ -2232,8 +2234,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } - userData.mBindingController.unbindCurrentMethod(); - return userData.mBindingController.bindCurrentMethod(); + mBindingController.unbindCurrentMethod(); + + return mBindingController.bindCurrentMethod(); } /** @@ -2253,18 +2256,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return currentMethodId; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); final int oldDeviceId = mDeviceIdToShowIme; mDeviceIdToShowIme = mVdmInternal.getDeviceIdForDisplayId(mDisplayIdToShowIme); if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { if (oldDeviceId == DEVICE_ID_DEFAULT) { return currentMethodId; } - final String defaultDeviceMethodId = settings.getSelectedDefaultDeviceInputMethod(); + final String defaultDeviceMethodId = mSettings.getSelectedDefaultDeviceInputMethod(); if (DEBUG) { Slog.v(TAG, "Restoring default device input method: " + defaultDeviceMethodId); } - settings.putSelectedDefaultDeviceInputMethod(null); + mSettings.putSelectedDefaultDeviceInputMethod(null); return defaultDeviceMethodId; } @@ -2272,7 +2274,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId); if (Objects.equals(deviceMethodId, currentMethodId)) { return currentMethodId; - } else if (!settings.getMethodMap().containsKey(deviceMethodId)) { + } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) { if (DEBUG) { Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme + " because its custom input method is not available: " + deviceMethodId); @@ -2284,7 +2286,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (DEBUG) { Slog.v(TAG, "Storing default device input method " + currentMethodId); } - settings.putSelectedDefaultDeviceInputMethod(currentMethodId); + mSettings.putSelectedDefaultDeviceInputMethod(currentMethodId); } if (DEBUG) { Slog.v(TAG, "Switching current input method from " + currentMethodId @@ -2314,8 +2316,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) { return false; } - final InputMethodInfo imi = InputMethodSettingsRepository.get(mCurrentUserId) - .getMethodMap().get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null) { return false; } @@ -2496,8 +2497,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub setSelectedMethodIdLocked(null); // Callback before clean-up binding states. onUnbindCurrentMethodByReset(); - final UserData userData = UserData.getOrCreate(mCurrentUserId); - userData.mBindingController.unbindCurrentMethod(); + mBindingController.unbindCurrentMethod(); unbindCurrentClientLocked(unbindClientReason); } @@ -2660,7 +2660,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (packageName != null) { if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, mCurrentUserId); + getPackageManagerForUser(mContext, mSettings.getUserId()); ApplicationInfo applicationInfo = null; try { applicationInfo = userAwarePackageManager.getApplicationInfo(packageName, @@ -2722,7 +2722,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded() - && mWindowManagerInternal.isKeyguardSecure(mCurrentUserId)) { + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId())) { return false; } if ((visibility & InputMethodService.IME_ACTIVE) == 0 @@ -2739,8 +2739,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - List<InputMethodInfo> imes = settings.getEnabledInputMethodListWithFilter( + List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter( InputMethodInfo::shouldShowInInputMethodPicker); final int numImes = imes.size(); if (numImes > 2) return true; @@ -2752,7 +2751,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = imes.get(i); final List<InputMethodSubtype> subtypes = - settings.getEnabledInputMethodSubtypeList(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); final int subtypeCount = subtypes.size(); if (subtypeCount == 0) { ++nonAuxCount; @@ -2904,12 +2903,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); if (enabledMayChange) { final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext, - settings.getUserId()); + mSettings.getUserId()); - List<InputMethodInfo> enabled = settings.getEnabledInputMethodList(); + List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); for (int i = 0; i < enabled.size(); i++) { // We allow the user to select "disabled until used" apps, so if they // are enabling one of those here we now need to make it enabled. @@ -2936,20 +2934,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { String ime = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_INPUT_METHOD, null, settings.getUserId()); + Settings.Secure.DEFAULT_INPUT_METHOD, null, mSettings.getUserId()); String defaultDeviceIme = SecureSettingsWrapper.getString( - Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, settings.getUserId()); + Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, mSettings.getUserId()); if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { if (DEBUG) { Slog.v(TAG, "Current input method " + ime + " differs from the stored default" - + " device input method for user " + settings.getUserId() + + " device input method for user " + mSettings.getUserId() + " - restoring " + defaultDeviceIme); } SecureSettingsWrapper.putString( Settings.Secure.DEFAULT_INPUT_METHOD, defaultDeviceIme, - settings.getUserId()); + mSettings.getUserId()); SecureSettingsWrapper.putString( - Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, settings.getUserId()); + Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null, mSettings.getUserId()); } } @@ -2957,14 +2955,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ENABLED_INPUT_METHODS is taking care of keeping them correctly in // sync, so we will never have a DEFAULT_INPUT_METHOD that is not // enabled. - String id = settings.getSelectedInputMethod(); + String id = mSettings.getSelectedInputMethod(); // There is no input method selected, try to choose new applicable input method. if (TextUtils.isEmpty(id) && chooseNewDefaultIMELocked()) { - id = settings.getSelectedInputMethod(); + id = mSettings.getSelectedInputMethod(); } if (!TextUtils.isEmpty(id)) { try { - setInputMethodLocked(id, settings.getSelectedInputMethodSubtypeId(id)); + setInputMethodLocked(id, mSettings.getSelectedInputMethodSubtypeId(id)); } catch (IllegalArgumentException e) { Slog.w(TAG, "Unknown input method from prefs: " + id, e); resetCurrentMethodAndClientLocked(UnbindReason.SWITCH_IME_FAILED); @@ -2975,18 +2973,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // TODO: Instantiate mSwitchingController for each user. - if (settings.getUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(settings.getMethodMap()); + if (mSettings.getUserId() == mSwitchingController.getUserId()) { + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, settings.getMethodMap(), settings.getUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (settings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(settings.getMethodMap()); + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - settings.getMethodMap(), settings.getUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); } @@ -3010,15 +3008,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void setInputMethodLocked(String id, int subtypeId, int deviceId) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - InputMethodInfo info = settings.getMethodMap().get(id); + InputMethodInfo info = mSettings.getMethodMap().get(id); if (info == null) { throw getExceptionForUnknownImeId(id); } // See if we need to notify a subtype change within the same IME. if (id.equals(getSelectedMethodIdLocked())) { - final int userId = settings.getUserId(); + final int userId = mSettings.getUserId(); final int subtypeCount = info.getSubtypeCount(); if (subtypeCount <= 0) { notifyInputMethodSubtypeChangedLocked(userId, info, null); @@ -3059,7 +3056,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // method is a custom one specific to a virtual device. So only update the settings // entry used to restore the default device input method once we want to show the IME // back on the default device. - settings.putSelectedDefaultDeviceInputMethod(id); + mSettings.putSelectedDefaultDeviceInputMethod(id); return; } IInputMethodInvoker curMethod = getCurMethodLocked(); @@ -3129,8 +3126,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable String delegatorPackageName, @NonNull IConnectionlessHandwritingCallback callback) throws RemoteException { synchronized (ImfLock.class) { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - if (!userData.mBindingController.supportsConnectionlessStylusHandwriting()) { + if (!mBindingController.supportsConnectionlessStylusHandwriting()) { Slog.w(TAG, "Connectionless stylus handwriting mode unsupported by IME."); callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED); return; @@ -3196,10 +3192,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + " startStylusHandwriting()"); return false; } - final UserData userData = UserData.getOrCreate(mCurrentUserId); final long ident = Binder.clearCallingIdentity(); try { - if (!userData.mBindingController.supportsStylusHandwriting()) { + if (!mBindingController.supportsStylusHandwriting()) { Slog.w(TAG, "Stylus HW unsupported by IME. Ignoring startStylusHandwriting()"); return false; @@ -3378,8 +3373,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer.requestImeVisibility(windowToken, true); // Ensure binding the connection when IME is going to show. - final UserData userData = UserData.getOrCreate(mCurrentUserId); - userData.mBindingController.setCurrentMethodVisible(); + mBindingController.setCurrentMethodVisible(); final IInputMethodInvoker curMethod = getCurMethodLocked(); ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); if (curMethod != null) { @@ -3482,8 +3476,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else { ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); } - final UserData userData = UserData.getOrCreate(mCurrentUserId); - userData.mBindingController.setCurrentMethodNotVisible(); + mBindingController.setCurrentMethodNotVisible(); mVisibilityStateComputer.clearImeShowFlags(); // Cancel existing statsToken for show IME as we got a hide request. ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); @@ -3574,7 +3567,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.USER_SWITCHING; } final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mCurrentUserId, false /* enabledOnly */); + mSettings.getUserId(), false /* enabledOnly */); for (int profileId : profileIdsWithDisabled) { if (profileId == userId) { scheduleSwitchUserTaskLocked(userId, cs.mClient); @@ -3620,10 +3613,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // Verify if caller is a background user. - if (userId != mCurrentUserId) { + final int currentUserId = mSettings.getUserId(); + if (userId != currentUserId) { if (ArrayUtils.contains( - mUserManagerInternal.getProfileIds(mCurrentUserId, false), - userId)) { + mUserManagerInternal.getProfileIds(currentUserId, false), userId)) { // cross-profile access is always allowed here to allow // profile-switching. scheduleSwitchUserTaskLocked(userId, cs.mClient); @@ -3765,8 +3758,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Note that we can trust client's display ID as long as it matches // to the display ID obtained from the window. if (cs.mSelfReportedDisplayId != mCurTokenDisplayId) { - final UserData userData = UserData.getOrCreate(userId); - userData.mBindingController.unbindCurrentMethod(); + mBindingController.unbindCurrentMethod(); } } } @@ -3813,7 +3805,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && mImeBindingState.mFocusedWindowClient.mClient.asBinder() == client.asBinder()) { return true; } - if (mCurrentUserId != UserHandle.getUserId(uid)) { + if (mSettings.getUserId() != UserHandle.getUserId(uid)) { return false; } if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid( @@ -3881,10 +3873,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - final InputMethodInfo imi = settings.getMethodMap().get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( - imi.getPackageName(), callingUid, userId, settings)) { + imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); } setInputMethodWithSubtypeIdLocked(token, id, NOT_A_SUBTYPE_ID); @@ -3900,10 +3891,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - final InputMethodInfo imi = settings.getMethodMap().get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( - imi.getPackageName(), callingUid, userId, settings)) { + imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); } if (subtype != null) { @@ -3921,11 +3911,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return false; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - final Pair<String, String> lastIme = settings.getLastInputMethodAndSubtype(); + final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype(); final InputMethodInfo lastImi; if (lastIme != null) { - lastImi = settings.getMethodMap().get(lastIme.first); + lastImi = mSettings.getMethodMap().get(lastIme.first); } else { lastImi = null; } @@ -3949,7 +3938,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // This is a safety net. If the currentSubtype can't be added to the history // and the framework couldn't find the last ime, we will make the last ime be // the most applicable enabled keyboard subtype of the system imes. - final List<InputMethodInfo> enabled = settings.getEnabledInputMethodList(); + final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); if (enabled != null) { final int enabledCount = enabled.size(); final String locale; @@ -3957,7 +3946,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && !TextUtils.isEmpty(mCurrentSubtype.getLocale())) { locale = mCurrentSubtype.getLocale(); } else { - locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); + locale = SystemLocaleWrapper.get(mSettings.getUserId()).get(0).toString(); } for (int i = 0; i < enabledCount; ++i) { final InputMethodInfo imi = enabled.get(i); @@ -4004,9 +3993,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - onlyCurrentIme, settings.getMethodMap().get(getSelectedMethodIdLocked()), + onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); if (nextSubtype == null) { return false; @@ -4022,10 +4010,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return false; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( false /* onlyCurrentIme */, - settings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); + mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); return nextSubtype != null; } } @@ -4037,7 +4024,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - return InputMethodSettingsRepository.get(userId).getLastInputMethodSubtype(); + if (mSettings.getUserId() == userId) { + return mSettings.getLastInputMethodSubtype(); + } + + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + return settings.getLastInputMethodSubtype(); } } @@ -4068,20 +4060,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } final var additionalSubtypeMap = AdditionalSubtypeMapRepository.get(userId); - final boolean isCurrentUser = (mCurrentUserId == userId); - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final boolean isCurrentUser = (mSettings.getUserId() == userId); + final InputMethodSettings settings = isCurrentUser + ? mSettings + : queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO); final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap( imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); if (additionalSubtypeMap != newAdditionalSubtypeMap) { AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap, settings.getMethodMap()); - final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext, - userId, AdditionalSubtypeMapRepository.get(userId), - DirectBootAwareness.AUTO); - InputMethodSettingsRepository.put(userId, newSettings); if (isCurrentUser) { final long ident = Binder.clearCallingIdentity(); try { + mSettings = queryInputMethodServicesInternal(mContext, + mSettings.getUserId(), + AdditionalSubtypeMapRepository.get(mSettings.getUserId()), + DirectBootAwareness.AUTO); postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */); } finally { Binder.restoreCallingIdentity(ident); @@ -4111,8 +4106,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (ImfLock.class) { - final boolean currentUser = (mCurrentUserId == userId); - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final boolean currentUser = (mSettings.getUserId() == userId); + final InputMethodSettings settings = currentUser + ? mSettings : queryMethodMapForUserLocked(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { return; } @@ -4233,10 +4229,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mStylusIds.add(deviceId); // a new Stylus is detected. If IME supports handwriting, and we don't have // handwriting initialized, lets do it now. - final UserData userData = UserData.getOrCreate(mCurrentUserId); - final InputMethodBindingController bindingController = userData.mBindingController; if (!mHwController.getCurrentRequestId().isPresent() - && bindingController.supportsStylusHandwriting()) { + && mBindingController.supportsStylusHandwriting()) { scheduleResetStylusHandwriting(); } } @@ -4465,11 +4459,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } return; } - if (mCurrentUserId != mSwitchingController.getUserId()) { + if (mSettings.getUserId() != mSwitchingController.getUserId()) { return; } - final InputMethodInfo imi = InputMethodSettingsRepository.get(mCurrentUserId) - .getMethodMap().get(getSelectedMethodIdLocked()); + final InputMethodInfo imi = + mSettings.getMethodMap().get(getSelectedMethodIdLocked()); if (imi != null) { mSwitchingController.onUserActionLocked(imi, mCurrentSubtype); } @@ -4529,9 +4523,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } else { // Called with current IME's token. - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - if (settings.getMethodMap().get(id) != null - && settings.getEnabledInputMethodListWithFilter( + if (mSettings.getMethodMap().get(id) != null + && mSettings.getEnabledInputMethodListWithFilter( (info) -> info.getId().equals(id)).isEmpty()) { throw new IllegalStateException("Requested IME is not enabled: " + id); } @@ -4710,23 +4703,21 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } synchronized (ImfLock.class) { - final InputMethodSettings settings = - InputMethodSettingsRepository.get(mCurrentUserId); final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() - && mWindowManagerInternal.isKeyguardSecure(settings.getUserId()); - final String lastInputMethodId = settings.getSelectedInputMethod(); + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId()); + final String lastInputMethodId = mSettings.getSelectedInputMethod(); int lastInputMethodSubtypeId = - settings.getSelectedInputMethodSubtypeId(lastInputMethodId); + mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( showAuxSubtypes, isScreenLocked, true /* forImeMenu */, - mContext, settings.getMethodMap(), settings.getUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); if (imList.isEmpty()) { Slog.w(TAG, "Show switching menu failed, imList is empty," + " showAuxSubtypes: " + showAuxSubtypes + " isScreenLocked: " + isScreenLocked - + " userId: " + settings.getUserId()); + + " userId: " + mSettings.getUserId()); return false; } @@ -4812,8 +4803,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub case MSG_RESET_HANDWRITING: { synchronized (ImfLock.class) { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - if (userData.mBindingController.supportsStylusHandwriting() + if (mBindingController.supportsStylusHandwriting() && getCurMethodLocked() != null && hasSupportedStylusLocked()) { Slog.d(TAG, "Initializing Handwriting Spy"); mHwController.initializeHandwritingSpy(mCurTokenDisplayId); @@ -4838,12 +4828,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (curMethod == null || mImeBindingState.mFocusedWindow == null) { return true; } - final UserData userData = UserData.getOrCreate(mCurrentUserId); final HandwritingModeController.HandwritingSession session = mHwController.startHandwritingSession( msg.arg1 /*requestId*/, msg.arg2 /*pid*/, - userData.mBindingController.getCurMethodUid(), + mBindingController.getCurMethodUid(), mImeBindingState.mFocusedWindow); if (session == null) { Slog.e(TAG, @@ -4914,9 +4903,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( - settings.getEnabledInputMethodList()); + mSettings.getEnabledInputMethodList()); if (imi != null) { if (DEBUG) { Slog.d(TAG, "New default IME was selected: " + imi.getId()); @@ -5030,8 +5018,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - // Construct the set of possible IME packages for onPackageChanged() to avoid false // negatives when the package state remains to be the same but only the component state is // changed. @@ -5042,7 +5028,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ResolveInfo> allInputMethodServices = mContext.getPackageManager().queryIntentServicesAsUser( new Intent(InputMethod.SERVICE_INTERFACE), - PackageManager.MATCH_DISABLED_COMPONENTS, settings.getUserId()); + PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getUserId()); final int numImes = allInputMethodServices.size(); for (int i = 0; i < numImes; ++i) { final ServiceInfo si = allInputMethodServices.get(i).serviceInfo; @@ -5057,11 +5043,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!resetDefaultEnabledIme) { boolean enabledImeFound = false; boolean enabledNonAuxImeFound = false; - final List<InputMethodInfo> enabledImes = settings.getEnabledInputMethodList(); + final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList(); final int numImes = enabledImes.size(); for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = enabledImes.get(i); - if (settings.getMethodMap().containsKey(imi.getId())) { + if (mSettings.getMethodMap().containsKey(imi.getId())) { enabledImeFound = true; if (!imi.isAuxiliaryIme()) { enabledNonAuxImeFound = true; @@ -5085,7 +5071,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodInfoUtils.getDefaultEnabledImes(mContext, settings.getMethodList(), + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(), reenableMinimumNonAuxSystemImes); final int numImes = defaultEnabledIme.size(); for (int i = 0; i < numImes; ++i) { @@ -5097,9 +5083,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } - final String defaultImiId = settings.getSelectedInputMethod(); + final String defaultImiId = mSettings.getSelectedInputMethod(); if (!TextUtils.isEmpty(defaultImiId)) { - if (!settings.getMethodMap().containsKey(defaultImiId)) { + if (!mSettings.getMethodMap().containsKey(defaultImiId)) { Slog.w(TAG, "Default IME is uninstalled. Choose new default IME."); if (chooseNewDefaultIMELocked()) { updateInputMethodsFromSettingsLocked(true); @@ -5113,32 +5099,31 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateDefaultVoiceImeIfNeededLocked(); // TODO: Instantiate mSwitchingController for each user. - if (settings.getUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(settings.getMethodMap()); + if (mSettings.getUserId() == mSwitchingController.getUserId()) { + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, settings.getMethodMap(), mCurrentUserId); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (settings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(settings.getMethodMap()); + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - settings.getMethodMap(), settings.getUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); // Notify InputMethodListListeners of the new installed InputMethods. - final List<InputMethodInfo> inputMethodList = settings.getMethodList(); + final List<InputMethodInfo> inputMethodList = mSettings.getMethodList(); mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED, - settings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget(); + mSettings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } @GuardedBy("ImfLock.class") void sendOnNavButtonFlagsChangedLocked() { - final UserData userData = UserData.getOrCreate(mCurrentUserId); - final IInputMethodInvoker curMethod = userData.mBindingController.getCurMethod(); + final IInputMethodInvoker curMethod = mBindingController.getCurMethod(); if (curMethod == null) { // No need to send the data if the IME is not yet bound. return; @@ -5148,12 +5133,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void updateDefaultVoiceImeIfNeededLocked() { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); final String systemSpeechRecognizer = mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); - final String currentDefaultVoiceImeId = settings.getDefaultVoiceInputMethod(); + final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( - settings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId); + mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked," @@ -5162,7 +5146,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Clear DEFAULT_VOICE_INPUT_METHOD when necessary. Note that InputMethodSettings // does not update the actual Secure Settings until the user is unlocked. if (!TextUtils.isEmpty(currentDefaultVoiceImeId)) { - settings.putDefaultVoiceInputMethod(""); + mSettings.putDefaultVoiceInputMethod(""); // We don't support disabling the voice ime when a package is removed from the // config. } @@ -5175,7 +5159,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.i(TAG, "Enabling the default Voice IME:" + newSystemVoiceIme); } setInputMethodEnabledLocked(newSystemVoiceIme.getId(), true); - settings.putDefaultVoiceInputMethod(newSystemVoiceIme.getId()); + mSettings.putDefaultVoiceInputMethod(newSystemVoiceIme.getId()); } // ---------------------------------------------------------------------- @@ -5190,9 +5174,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private boolean setInputMethodEnabledLocked(String id, boolean enabled) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); if (enabled) { - final String enabledImeIdsStr = settings.getEnabledInputMethodsStr(); + final String enabledImeIdsStr = mSettings.getEnabledInputMethodsStr(); final String newEnabledImeIdsStr = InputMethodUtils.concatEnabledImeIds( enabledImeIdsStr, id); if (TextUtils.equals(enabledImeIdsStr, newEnabledImeIdsStr)) { @@ -5200,29 +5183,29 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Nothing to do. The previous state was enabled. return true; } - settings.putEnabledInputMethodsStr(newEnabledImeIdsStr); + mSettings.putEnabledInputMethodsStr(newEnabledImeIdsStr); // Previous state was disabled. return false; } else { - final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = settings + final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings .getEnabledInputMethodsAndSubtypeList(); StringBuilder builder = new StringBuilder(); - if (settings.buildAndPutEnabledInputMethodsStrRemovingId( + if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId( builder, enabledInputMethodsList, id)) { if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { // Disabled input method is currently selected, switch to another one. - final String selId = settings.getSelectedInputMethod(); + final String selId = mSettings.getSelectedInputMethod(); if (id.equals(selId) && !chooseNewDefaultIMELocked()) { Slog.i(TAG, "Can't find new IME, unsetting the current input method."); resetSelectedInputMethodAndSubtypeLocked(""); } - } else if (id.equals(settings.getSelectedDefaultDeviceInputMethod())) { + } else if (id.equals(mSettings.getSelectedDefaultDeviceInputMethod())) { // Disabled default device IME while using a virtual device one, choose a // new default one but only update the settings. InputMethodInfo newDefaultIme = InputMethodInfoUtils.getMostApplicableDefaultIME( - settings.getEnabledInputMethodList()); - settings.putSelectedDefaultDeviceInputMethod( + mSettings.getEnabledInputMethodList()); + mSettings.putSelectedDefaultDeviceInputMethod( newDefaultIme == null ? null : newDefaultIme.getId()); } // Previous state was enabled. @@ -5238,30 +5221,29 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId, boolean setSubtypeOnly) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - settings.saveCurrentInputMethodAndSubtypeToHistory(getSelectedMethodIdLocked(), + mSettings.saveCurrentInputMethodAndSubtypeToHistory(getSelectedMethodIdLocked(), mCurrentSubtype); // Set Subtype here if (imi == null || subtypeId < 0) { - settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); + mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID); mCurrentSubtype = null; } else { if (subtypeId < imi.getSubtypeCount()) { InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId); - settings.putSelectedSubtype(subtype.hashCode()); + mSettings.putSelectedSubtype(subtype.hashCode()); mCurrentSubtype = subtype; } else { - settings.putSelectedSubtype(NOT_A_SUBTYPE_ID); + mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID); // If the subtype is not specified, choose the most applicable one mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); } } - notifyInputMethodSubtypeChangedLocked(settings.getUserId(), imi, mCurrentSubtype); + notifyInputMethodSubtypeChangedLocked(mSettings.getUserId(), imi, mCurrentSubtype); if (!setSubtypeOnly) { // Set InputMethod here - settings.putSelectedInputMethod(imi != null ? imi.getId() : ""); + mSettings.putSelectedInputMethod(imi != null ? imi.getId() : ""); } } @@ -5269,15 +5251,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) { mDeviceIdToShowIme = DEVICE_ID_DEFAULT; mDisplayIdToShowIme = INVALID_DISPLAY; + mSettings.putSelectedDefaultDeviceInputMethod(null); - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - settings.putSelectedDefaultDeviceInputMethod(null); - - InputMethodInfo imi = settings.getMethodMap().get(newDefaultIme); + InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme); int lastSubtypeId = NOT_A_SUBTYPE_ID; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { - String subtypeHashCode = settings.getLastSubtypeForInputMethod(newDefaultIme); + String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, @@ -5304,12 +5284,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mCurrentUserId == userId) { + if (mSettings.getUserId() == userId) { return getCurrentInputMethodSubtypeLocked(); } - return InputMethodSettingsRepository.get(userId) - .getCurrentInputMethodSubtypeForNonCurrentUsers(); + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + return settings.getCurrentInputMethodSubtypeForNonCurrentUsers(); } } @@ -5329,27 +5309,26 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (selectedMethodId == null) { return null; } - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - final boolean subtypeIsSelected = settings.isSubtypeSelected(); - final InputMethodInfo imi = settings.getMethodMap().get(selectedMethodId); + final boolean subtypeIsSelected = mSettings.isSubtypeSelected(); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null || imi.getSubtypeCount() == 0) { return null; } if (!subtypeIsSelected || mCurrentSubtype == null || !SubtypeUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) { - int subtypeId = settings.getSelectedInputMethodSubtypeId(selectedMethodId); + int subtypeId = mSettings.getSelectedInputMethodSubtypeId(selectedMethodId); if (subtypeId == NOT_A_SUBTYPE_ID) { // If there are no selected subtypes, the framework will try to find // the most applicable subtype from explicitly or implicitly enabled // subtypes. List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - settings.getEnabledInputMethodSubtypeList(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, // just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { - final String locale = SystemLocaleWrapper.get(settings.getUserId()) + final String locale = SystemLocaleWrapper.get(mSettings.getUserId()) .get(0).toString(); mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, @@ -5372,22 +5351,38 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); + final InputMethodSettings settings; + if (userId == mSettings.getUserId()) { + settings = mSettings; + } else { + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeMapRepository.get(userId); + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); + } return settings.getMethodMap().get(settings.getSelectedInputMethod()); } @GuardedBy("ImfLock.class") + private InputMethodSettings queryMethodMapForUserLocked(@UserIdInt int userId) { + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeMapRepository.get(userId); + return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO); + } + + @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (userId == mCurrentUserId) { - if (!settings.getMethodMap().containsKey(imeId) - || !settings.getEnabledInputMethodList() - .contains(settings.getMethodMap().get(imeId))) { + if (userId == mSettings.getUserId()) { + if (!mSettings.getMethodMap().containsKey(imeId) + || !mSettings.getEnabledInputMethodList() + .contains(mSettings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID); return true; } + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); if (!settings.getMethodMap().containsKey(imeId) || !settings.getEnabledInputMethodList().contains( settings.getMethodMap().get(imeId))) { @@ -5428,9 +5423,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void switchKeyboardLayoutLocked(int direction) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); - - final InputMethodInfo currentImi = settings.getMethodMap().get(getSelectedMethodIdLocked()); + final InputMethodInfo currentImi = mSettings.getMethodMap().get( + getSelectedMethodIdLocked()); if (currentImi == null) { return; } @@ -5442,7 +5436,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (nextSubtypeHandle == null) { return; } - final InputMethodInfo nextImi = settings.getMethodMap().get(nextSubtypeHandle.getImeId()); + final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId()); if (nextImi == null) { return; } @@ -5521,14 +5515,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) { synchronized (ImfLock.class) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (!settings.getMethodMap().containsKey(imeId)) { - return false; // IME is not found. - } - if (userId == mCurrentUserId) { + if (userId == mSettings.getUserId()) { + if (!mSettings.getMethodMap().containsKey(imeId)) { + return false; // IME is not found. + } setInputMethodEnabledLocked(imeId, enabled); return true; } + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); + if (!settings.getMethodMap().containsKey(imeId)) { + return false; // IME is not found. + } if (enabled) { final String enabledImeIdsStr = settings.getEnabledInputMethodsStr(); final String newEnabledImeIdsStr = InputMethodUtils.concatEnabledImeIds( @@ -5863,9 +5860,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final Printer p = new PrintWriterPrinter(pw); synchronized (ImfLock.class) { - final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId); p.println("Current Input Method Manager state:"); - final List<InputMethodInfo> methodList = settings.getMethodList(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); int numImes = methodList.size(); p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount); for (int i = 0; i < numImes; i++) { @@ -5889,16 +5885,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" curSession=" + c.mCurSession); }; mClientController.forAllClients(clientControllerDump); - p.println(" mCurrentUserId=" + mCurrentUserId); p.println(" mCurMethodId=" + getSelectedMethodIdLocked()); client = mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked()); p.println(" mFocusedWindowPerceptible=" + mFocusedWindowPerceptible); mImeBindingState.dump(" ", p); - final UserData userData = UserData.getOrCreate(mCurrentUserId); p.println(" mCurId=" + getCurIdLocked() + " mHaveConnection=" + hasConnectionLocked() + " mBoundToMethod=" + mBoundToMethod + " mVisibleBound=" - + userData.mBindingController.isVisibleBound()); + + mBindingController.isVisibleBound()); p.println(" mCurToken=" + getCurTokenLocked()); p.println(" mCurTokenDisplayId=" + mCurTokenDisplayId); p.println(" mCurHostInputToken=" + mCurHostInputToken); @@ -5916,6 +5910,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ? Arrays.toString(mStylusIds.toArray()) : "")); p.println(" mSwitchingController:"); mSwitchingController.dump(p); + p.println(" mSettings:"); + mSettings.dump(p, " "); p.println(" mStartInputHistory:"); mStartInputHistory.dump(pw, " "); @@ -6170,7 +6166,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); try (PrintWriter pr = shellCommand.getOutPrintWriter()) { for (int userId : userIds) { final List<InputMethodInfo> methods = all @@ -6215,7 +6211,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6274,14 +6270,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error) { boolean failedToEnableUnknownIme = false; boolean previouslyEnabled = false; - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (userId == mCurrentUserId) { - if (enabled && !settings.getMethodMap().containsKey(imeId)) { + if (userId == mSettings.getUserId()) { + if (enabled && !mSettings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled); } } else { + final InputMethodSettings settings = queryMethodMapForUserLocked(userId); if (enabled) { if (!settings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; @@ -6336,7 +6332,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6376,7 +6372,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { try (PrintWriter out = shellCommand.getOutPrintWriter()) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mCurrentUserId, shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6388,17 +6384,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } final String nextIme; final List<InputMethodInfo> nextEnabledImes; - final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (userId == mCurrentUserId) { + if (userId == mSettings.getUserId()) { hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */, SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND); - final UserData userData = UserData.getOrCreate(mCurrentUserId); - userData.mBindingController.unbindCurrentMethod(); + mBindingController.unbindCurrentMethod(); // Enable default IMEs, disable others - var toDisable = settings.getEnabledInputMethodList(); + var toDisable = mSettings.getEnabledInputMethodList(); var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes( - mContext, settings.getMethodList()); + mContext, mSettings.getMethodList()); toDisable.removeAll(defaultEnabled); for (InputMethodInfo info : toDisable) { setInputMethodEnabledLocked(info.getId(), false); @@ -6412,11 +6406,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( - getPackageManagerForUser(mContext, settings.getUserId()), - settings.getEnabledInputMethodList()); - nextIme = settings.getSelectedInputMethod(); - nextEnabledImes = settings.getEnabledInputMethodList(); + getPackageManagerForUser(mContext, mSettings.getUserId()), + mSettings.getEnabledInputMethodList()); + nextIme = mSettings.getSelectedInputMethod(); + nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { + final AdditionalSubtypeMap additionalSubtypeMap = + AdditionalSubtypeMapRepository.get(userId); + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); + nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, settings.getMethodList()); nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java b/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java deleted file mode 100644 index 60b9a4cfe840..000000000000 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettingsRepository.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.inputmethod; - -import android.annotation.NonNull; -import android.annotation.UserIdInt; -import android.content.Context; -import android.content.pm.UserInfo; -import android.os.Handler; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.inputmethod.DirectBootAwareness; -import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; - -final class InputMethodSettingsRepository { - @GuardedBy("ImfLock.class") - @NonNull - private static final SparseArray<InputMethodSettings> sPerUserMap = new SparseArray<>(); - - /** - * Not intended to be instantiated. - */ - private InputMethodSettingsRepository() { - } - - @NonNull - @GuardedBy("ImfLock.class") - static InputMethodSettings get(@UserIdInt int userId) { - final InputMethodSettings obj = sPerUserMap.get(userId); - if (obj != null) { - return obj; - } - return InputMethodSettings.createEmptyMap(userId); - } - - @GuardedBy("ImfLock.class") - static void put(@UserIdInt int userId, @NonNull InputMethodSettings obj) { - sPerUserMap.put(userId, obj); - } - - static void initialize(@NonNull Handler handler, @NonNull Context context) { - final UserManagerInternal userManagerInternal = - LocalServices.getService(UserManagerInternal.class); - handler.post(() -> { - userManagerInternal.addUserLifecycleListener( - new UserManagerInternal.UserLifecycleListener() { - @Override - public void onUserRemoved(UserInfo user) { - final int userId = user.id; - handler.post(() -> { - synchronized (ImfLock.class) { - sPerUserMap.remove(userId); - } - }); - } - }); - synchronized (ImfLock.class) { - for (int userId : userManagerInternal.getUserIds()) { - final InputMethodSettings settings = - InputMethodManagerService.queryInputMethodServicesInternal( - context, - userId, - AdditionalSubtypeMapRepository.get(userId), - DirectBootAwareness.AUTO); - sPerUserMap.put(userId, settings); - } - } - }); - } -} diff --git a/services/core/java/com/android/server/inputmethod/Sequence.java b/services/core/java/com/android/server/inputmethod/Sequence.java deleted file mode 100644 index 05e31ce1c682..000000000000 --- a/services/core/java/com/android/server/inputmethod/Sequence.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.inputmethod; - -import com.android.internal.annotations.GuardedBy; - -/** - * A sequence number utility class that only generate positive numbers. - */ -final class Sequence { - - private final Object mLock = new Object(); - - private int mSequence; - - int getSequenceNumber() { - synchronized (mLock) { - return mSequence; - } - } - - @GuardedBy("ImfLock.class") - void advanceSequenceNumber() { - synchronized (mLock) { - mSequence++; - if (mSequence <= 0) { - mSequence = 1; - } - } - } -} diff --git a/services/core/java/com/android/server/inputmethod/UserData.java b/services/core/java/com/android/server/inputmethod/UserData.java deleted file mode 100644 index 8e20611ea75b..000000000000 --- a/services/core/java/com/android/server/inputmethod/UserData.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.inputmethod; - -import android.annotation.NonNull; -import android.annotation.UserIdInt; -import android.content.pm.UserInfo; -import android.os.Handler; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; - -final class UserData { - - private static SparseArray<UserData> sUserData; - - @GuardedBy("ImfLock.class") - private static InputMethodBindingController.Creator sBindingControllerCreator; - - @UserIdInt - final int mUserId; - - @GuardedBy("ImfLock.class") - final Sequence mSequence = new Sequence(); - - @NonNull - final InputMethodBindingController mBindingController; - - /** - * Not intended to be instantiated. - */ - private UserData(int userId, - InputMethodBindingController bindingController) { - mUserId = userId; - mBindingController = bindingController; - } - - @GuardedBy("ImfLock.class") - static UserData getOrCreate(@UserIdInt int userId) { - UserData userData = sUserData.get(userId); - if (userData == null) { - userData = new UserData(userId, sBindingControllerCreator.create()); - sUserData.put(userId, userData); - } - return userData; - } - - @GuardedBy("ImfLock.class") - static void initialize(Handler handler, - InputMethodBindingController.Creator bindingControllerCreator) { - sUserData = new SparseArray<>(); - sBindingControllerCreator = bindingControllerCreator; - final UserManagerInternal userManagerInternal = - LocalServices.getService(UserManagerInternal.class); - userManagerInternal.addUserLifecycleListener( - new UserManagerInternal.UserLifecycleListener() { - @Override - public void onUserRemoved(UserInfo user) { - final int userId = user.id; - handler.post(() -> { - synchronized (ImfLock.class) { - sUserData.remove(userId); - } - }); - } - - @Override - public void onUserCreated(UserInfo user, Object unusedToken) { - final int userId = user.id; - handler.post(() -> { - synchronized (ImfLock.class) { - getOrCreate(userId); - } - }); - } - }); - synchronized (ImfLock.class) { - for (int userId : userManagerInternal.getUserIds()) { - getOrCreate(userId); - } - } - } -} diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 1dc86f2b7f1e..0cd7654f70ea 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -17,6 +17,7 @@ package com.android.server.media; import android.app.ForegroundServiceDelegationOptions; +import android.app.Notification; import android.media.MediaController2; import android.media.Session2CommandGroup; import android.media.Session2Token; @@ -169,6 +170,12 @@ public class MediaSession2Record extends MediaSessionRecordImpl { } @Override + boolean isLinkedToNotification(Notification notification) { + // Currently it's not possible to link MediaSession2 with a Notification + return false; + } + + @Override public int getSessionPolicies() { synchronized (mLock) { return mPolicies; diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 5b3934ea9b13..ce31ac84cbe3 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -30,6 +30,7 @@ import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ForegroundServiceDelegationOptions; +import android.app.Notification; import android.app.PendingIntent; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; @@ -89,6 +90,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -639,6 +641,15 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } @Override + boolean isLinkedToNotification(Notification notification) { + return notification.isMediaNotification() + && Objects.equals( + notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, MediaSession.Token.class), + mSessionToken); + } + + @Override public int getSessionPolicies() { synchronized (mLock) { return mPolicies; diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java index e4b2fad5f309..09991995099e 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java @@ -17,6 +17,7 @@ package com.android.server.media; import android.app.ForegroundServiceDelegationOptions; +import android.app.Notification; import android.media.AudioManager; import android.media.session.PlaybackState; import android.os.ResultReceiver; @@ -153,6 +154,9 @@ public abstract class MediaSessionRecordImpl { */ public abstract boolean canHandleVolumeKey(); + /** Returns whether this session is linked to the passed notification. */ + abstract boolean isLinkedToNotification(Notification notification); + /** * Get session policies from custom policy provider set when MediaSessionRecord is instantiated. * If custom policy does not exist, will return null. diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index e2163c54f4e2..53c32cf31d21 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -32,6 +32,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ForegroundServiceDelegationOptions; import android.app.KeyguardManager; +import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.usage.UsageStatsManager; @@ -81,6 +82,8 @@ import android.os.ShellCallback; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; import android.speech.RecognizerIntent; import android.text.TextUtils; import android.util.Log; @@ -105,6 +108,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -136,9 +140,9 @@ public class MediaSessionService extends SystemService implements Monitor { /** * Action reported to UsageStatsManager when a media session is no longer active and user * engaged for a given app. If media session only pauses for a brief time the event will not - * necessarily be reported in case user is still "engaging" and will restart it momentarily. + * necessarily be reported in case user is still "engaged" and will restart it momentarily. * In such case, action may be reported after a short delay to ensure user is truly no longer - * engaging. Afterwards, the app is no longer expected to show an ongoing notification. + * engaged. Afterwards, the app is no longer expected to show an ongoing notification. */ private static final String USAGE_STATS_ACTION_STOP = "stop"; private static final String USAGE_STATS_CATEGORY = "android.media"; @@ -164,14 +168,35 @@ public class MediaSessionService extends SystemService implements Monitor { private KeyguardManager mKeyguardManager; private AudioManager mAudioManager; + private NotificationListener mNotificationListener; private boolean mHasFeatureLeanback; private ActivityManagerInternal mActivityManagerInternal; private UsageStatsManagerInternal mUsageStatsManagerInternal; - /* Maps uid with all user engaging session tokens associated to it */ - private final SparseArray<Set<MediaSessionRecordImpl>> mUserEngagingSessions = + /** + * Maps uid with all user engaged session records associated to it. It's used for logging start + * and stop events to UsageStatsManagerInternal. This collection contains MediaSessionRecord(s) + * and MediaSession2Record(s). + * When the media session is paused, the stop event is being logged immediately unlike fgs which + * waits for a certain timeout before considering it disengaged. + */ + private final SparseArray<Set<MediaSessionRecordImpl>> mUserEngagedSessionsForUsageLogging = new SparseArray<>(); + /** + * Maps uid with all user engaged session records associated to it. It's used for calling + * ActivityManagerInternal startFGS and stopFGS. This collection doesn't contain + * MediaSession2Record(s). When the media session is paused, There exists a timeout before + * calling stopFGS unlike usage logging which considers it disengaged immediately. + */ + @GuardedBy("mLock") + private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs = + new HashMap<>(); + + /* Maps uid with all media notifications associated to it */ + @GuardedBy("mLock") + private final Map<Integer, Set<Notification>> mMediaNotifications = new HashMap<>(); + // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile) // It's always not null after the MediaSessionService is started. private FullUserRecord mCurrentFullUserRecord; @@ -228,6 +253,7 @@ public class MediaSessionService extends SystemService implements Monitor { mMediaEventWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "handleMediaEvent"); mNotificationManager = mContext.getSystemService(NotificationManager.class); mAudioManager = mContext.getSystemService(AudioManager.class); + mNotificationListener = new NotificationListener(); } @Override @@ -283,6 +309,16 @@ public class MediaSessionService extends SystemService implements Monitor { mCommunicationManager = mContext.getSystemService(MediaCommunicationManager.class); mCommunicationManager.registerSessionCallback(new HandlerExecutor(mHandler), mSession2TokenCallback); + if (Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + try { + mNotificationListener.registerAsSystemService( + mContext, + new ComponentName(mContext, NotificationListener.class), + UserHandle.USER_ALL); + } catch (RemoteException e) { + // Intra-process call, should never happen. + } + } break; case PHASE_ACTIVITY_MANAGER_READY: MediaSessionDeviceConfig.initialize(mContext); @@ -630,11 +666,52 @@ public class MediaSessionService extends SystemService implements Monitor { return; } if (allowRunningInForeground) { - mActivityManagerInternal.startForegroundServiceDelegate( - foregroundServiceDelegationOptions, /* connection= */ null); + onUserSessionEngaged(record); } else { - mActivityManagerInternal.stopForegroundServiceDelegate( - foregroundServiceDelegationOptions); + onUserDisengaged(record); + } + } + + private void onUserSessionEngaged(MediaSessionRecordImpl mediaSessionRecord) { + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); + mUserEngagedSessionsForFgs.putIfAbsent(uid, new HashSet<>()); + mUserEngagedSessionsForFgs.get(uid).add(mediaSessionRecord); + for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, Set.of())) { + if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { + mActivityManagerInternal.startForegroundServiceDelegate( + mediaSessionRecord.getForegroundServiceDelegationOptions(), + /* connection= */ null); + return; + } + } + } + } + + private void onUserDisengaged(MediaSessionRecordImpl mediaSessionRecord) { + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); + if (mUserEngagedSessionsForFgs.containsKey(uid)) { + mUserEngagedSessionsForFgs.get(uid).remove(mediaSessionRecord); + if (mUserEngagedSessionsForFgs.get(uid).isEmpty()) { + mUserEngagedSessionsForFgs.remove(uid); + } + } + + boolean shouldStopFgs = true; + for (MediaSessionRecordImpl sessionRecord : + mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { + for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, + Set.of())) { + if (sessionRecord.isLinkedToNotification(mediaNotification)) { + shouldStopFgs = false; + } + } + } + if (shouldStopFgs) { + mActivityManagerInternal.stopForegroundServiceDelegate( + mediaSessionRecord.getForegroundServiceDelegationOptions()); + } } } @@ -646,18 +723,18 @@ public class MediaSessionService extends SystemService implements Monitor { String packageName = record.getPackageName(); int sessionUid = record.getUid(); if (userEngaged) { - if (!mUserEngagingSessions.contains(sessionUid)) { - mUserEngagingSessions.put(sessionUid, new HashSet<>()); + if (!mUserEngagedSessionsForUsageLogging.contains(sessionUid)) { + mUserEngagedSessionsForUsageLogging.put(sessionUid, new HashSet<>()); reportUserInteractionEvent( - USAGE_STATS_ACTION_START, record.getUserId(), packageName); + USAGE_STATS_ACTION_START, record.getUserId(), packageName); } - mUserEngagingSessions.get(sessionUid).add(record); - } else if (mUserEngagingSessions.contains(sessionUid)) { - mUserEngagingSessions.get(sessionUid).remove(record); - if (mUserEngagingSessions.get(sessionUid).isEmpty()) { + mUserEngagedSessionsForUsageLogging.get(sessionUid).add(record); + } else if (mUserEngagedSessionsForUsageLogging.contains(sessionUid)) { + mUserEngagedSessionsForUsageLogging.get(sessionUid).remove(record); + if (mUserEngagedSessionsForUsageLogging.get(sessionUid).isEmpty()) { reportUserInteractionEvent( - USAGE_STATS_ACTION_STOP, record.getUserId(), packageName); - mUserEngagingSessions.remove(sessionUid); + USAGE_STATS_ACTION_STOP, record.getUserId(), packageName); + mUserEngagedSessionsForUsageLogging.remove(sessionUid); } } } @@ -3043,4 +3120,88 @@ public class MediaSessionService extends SystemService implements Monitor { obtainMessage(msg, userIdInteger).sendToTarget(); } } + + private final class NotificationListener extends NotificationListenerService { + @Override + public void onNotificationPosted(StatusBarNotification sbn) { + super.onNotificationPosted(sbn); + Notification postedNotification = sbn.getNotification(); + int uid = sbn.getUid(); + + if (!postedNotification.isMediaNotification()) { + return; + } + synchronized (mLock) { + mMediaNotifications.putIfAbsent(uid, new HashSet<>()); + mMediaNotifications.get(uid).add(postedNotification); + for (MediaSessionRecordImpl mediaSessionRecord : + mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = + mediaSessionRecord.getForegroundServiceDelegationOptions(); + if (mediaSessionRecord.isLinkedToNotification(postedNotification) + && foregroundServiceDelegationOptions != null) { + mActivityManagerInternal.startForegroundServiceDelegate( + foregroundServiceDelegationOptions, + /* connection= */ null); + return; + } + } + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn) { + super.onNotificationRemoved(sbn); + Notification removedNotification = sbn.getNotification(); + int uid = sbn.getUid(); + if (!removedNotification.isMediaNotification()) { + return; + } + synchronized (mLock) { + Set<Notification> uidMediaNotifications = mMediaNotifications.get(uid); + if (uidMediaNotifications != null) { + uidMediaNotifications.remove(removedNotification); + if (uidMediaNotifications.isEmpty()) { + mMediaNotifications.remove(uid); + } + } + + MediaSessionRecordImpl notificationRecord = + getLinkedMediaSessionRecord(uid, removedNotification); + + if (notificationRecord == null) { + return; + } + + boolean shouldStopFgs = true; + for (MediaSessionRecordImpl mediaSessionRecord : + mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { + for (Notification mediaNotification : + mMediaNotifications.getOrDefault(uid, Set.of())) { + if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { + shouldStopFgs = false; + } + } + } + if (shouldStopFgs + && notificationRecord.getForegroundServiceDelegationOptions() != null) { + mActivityManagerInternal.stopForegroundServiceDelegate( + notificationRecord.getForegroundServiceDelegationOptions()); + } + } + } + + private MediaSessionRecordImpl getLinkedMediaSessionRecord( + int uid, Notification notification) { + synchronized (mLock) { + for (MediaSessionRecordImpl mediaSessionRecord : + mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { + if (mediaSessionRecord.isLinkedToNotification(notification)) { + return mediaSessionRecord; + } + } + } + return null; + } + } } 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..ca3db2c90815 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; @@ -1270,17 +1237,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 +1378,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 +1646,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 +1779,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 +1968,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) { @@ -2164,8 +2043,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 +2065,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 +2091,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 +2209,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 +2224,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 +2271,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 +2290,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 +2300,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 +2317,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 +2449,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 +2467,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 +2478,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 +2497,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 +2630,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 +2809,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 +2828,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 +6612,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 +6780,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 +8075,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 +8211,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 +8490,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 +8511,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 +8839,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 +9110,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 +9380,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 +9464,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 +9813,7 @@ public class NotificationManagerService extends SystemService { cancellationElapsedTimeMs); } } - if (Flags.refactorAttentionHelper()) { - mAttentionHelper.updateLightsLocked(); - } else { - updateLightsLocked(); - } + mAttentionHelper.updateLightsLocked(); } } @@ -10745,37 +9962,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 +10160,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 +10530,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/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index c559892327df..e394482fdda9 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -36,6 +36,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_UID_CHANGED; import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE; import static android.content.pm.PackageManager.INSTALL_STAGED; import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; +import static android.content.pm.PackageManager.PROPERTY_ANDROID_SAFETY_LABEL_PATH; import static android.content.pm.PackageManager.UNINSTALL_REASON_UNKNOWN; import static android.content.pm.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V4; import static android.content.pm.parsing.ApkLiteParseUtils.isApkFile; @@ -45,8 +46,8 @@ import static android.os.storage.StorageManager.FLAG_STORAGE_CE; import static android.os.storage.StorageManager.FLAG_STORAGE_DE; import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL; -import static com.android.internal.pm.pkg.parsing.ParsingPackageUtils.APP_METADATA_FILE_NAME; import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; +import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; import static com.android.server.pm.PackageManagerService.DEBUG_PACKAGE_SCANNING; @@ -2206,8 +2207,9 @@ final class InstallPackageHelper { ps.setAppMetadataSource(APP_METADATA_SOURCE_INSTALLER); } } else { + Map<String, PackageManager.Property> properties = parsedPackage.getProperties(); if (Flags.aslInApkAppMetadataSource() - && parsedPackage.isAppMetadataFileInApk()) { + && properties.containsKey(PROPERTY_ANDROID_SAFETY_LABEL_PATH)) { ps.setAppMetadataFilePath(appMetadataFile.getAbsolutePath()); ps.setAppMetadataSource(APP_METADATA_SOURCE_APK); } else { diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 20b669b96609..4c95e8305c17 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -104,6 +104,7 @@ import android.os.ShellCallback; import android.os.ShellCommand; import android.os.UserHandle; import android.os.UserManager; +import android.provider.DeviceConfig; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -161,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, @@ -215,7 +215,9 @@ public class LauncherAppsService extends SystemService { static class LauncherAppsImpl extends ILauncherApps.Stub { private static final boolean DEBUG = false; private static final String TAG = "LauncherAppsService"; - + private static final String NAMESPACE_MULTIUSER = "multiuser"; + private static final String FLAG_NON_SYSTEM_ACCESS_TO_HIDDEN_PROFILES = + "allow_3p_launchers_access_via_launcher_apps_apis"; private final Context mContext; private final UserManager mUm; private final RoleManager mRoleManager; @@ -563,6 +565,10 @@ public class LauncherAppsService extends SystemService { return true; } + if (isAccessToHiddenProfilesForNonSystemAppsForbidden()) { + return false; + } + if (!mRoleManager .getRoleHoldersAsUser( RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) @@ -570,7 +576,6 @@ public class LauncherAppsService extends SystemService { return false; } - // TODO(b/321988638): add option to disable with a flag return mContext.checkPermission( android.Manifest.permission.ACCESS_HIDDEN_PROFILES, callingPid, @@ -581,6 +586,13 @@ public class LauncherAppsService extends SystemService { } } + private boolean isAccessToHiddenProfilesForNonSystemAppsForbidden() { + return !DeviceConfig.getBoolean( + NAMESPACE_MULTIUSER, + FLAG_NON_SYSTEM_ACCESS_TO_HIDDEN_PROFILES, + /* defaultValue= */ true); + } + private boolean areHiddenApisChecksEnabled() { return android.os.Flags.allowPrivateProfile() && Flags.enableHidingProfiles() @@ -1788,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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 9a2b98f316c4..095a233bde64 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -593,7 +593,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService static final char RANDOM_CODEPATH_PREFIX = '-'; public static final String APP_METADATA_FILE_NAME = "app.metadata"; - public static final String APP_METADATA_FILE_IN_APK_PATH = "assets/" + APP_METADATA_FILE_NAME; static final int DEFAULT_FILE_ACCESS_MODE = 0644; @@ -5234,8 +5233,13 @@ public class PackageManagerService implements PackageSender, TestUtilityService File file = new File(filePath); if (Flags.aslInApkAppMetadataSource() && !file.exists() && ps.getAppMetadataSource() == APP_METADATA_SOURCE_APK) { - String apkPath = ps.getPkg().getSplits().get(0).getPath(); - if (!PackageManagerServiceUtils.extractAppMetadataFromApk(apkPath, file)) { + AndroidPackageInternal pkg = ps.getPkg(); + if (pkg == null) { + Slog.w(TAG, "Unable to to extract app metadata for " + packageName + + ". APK missing from device"); + return null; + } + if (!PackageManagerServiceUtils.extractAppMetadataFromApk(pkg, file)) { if (file.exists()) { file.delete(); } @@ -6257,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/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 189a138a191a..110a29c4ee58 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE; import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE; import static android.content.pm.PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE; +import static android.content.pm.PackageManager.PROPERTY_ANDROID_SAFETY_LABEL_PATH; import static android.content.pm.SigningDetails.CertCapabilities.SHARED_USER_ID; import static android.system.OsConstants.O_CREAT; import static android.system.OsConstants.O_RDWR; @@ -29,7 +30,6 @@ import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_RE import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; import static com.android.server.pm.PackageInstallerSession.APP_METADATA_FILE_ACCESS_MODE; import static com.android.server.pm.PackageInstallerSession.getAppMetadataSizeLimit; -import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_IN_APK_PATH; import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; @@ -60,6 +60,7 @@ import android.content.pm.ComponentInfo; import android.content.pm.PackageInfoLite; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.Property; import android.content.pm.PackagePartitions; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -111,6 +112,7 @@ import com.android.server.am.ActivityManagerUtils; import com.android.server.compat.PlatformCompat; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.AndroidPackageSplit; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.resolution.ComponentResolverApi; import com.android.server.pm.verify.domain.DomainVerificationManagerInternal; @@ -140,13 +142,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; +import java.util.zip.ZipFile; /** * Class containing helper methods for the PackageManagerService. @@ -1567,30 +1570,35 @@ public class PackageManagerServiceUtils { /** * Extract the app.metadata file from apk. */ - public static boolean extractAppMetadataFromApk(String apkPath, File appMetadataFile) { - boolean found = false; - try (ZipInputStream zipInputStream = - new ZipInputStream(new FileInputStream(new File(apkPath)))) { - ZipEntry zipEntry = null; - while ((zipEntry = zipInputStream.getNextEntry()) != null) { - if (zipEntry.getName().equals(APP_METADATA_FILE_IN_APK_PATH)) { - found = true; - try (FileOutputStream out = new FileOutputStream(appMetadataFile)) { - FileUtils.copy(zipInputStream, out); - } - if (appMetadataFile.length() > getAppMetadataSizeLimit()) { - appMetadataFile.delete(); - return false; + public static boolean extractAppMetadataFromApk(AndroidPackage pkg, File appMetadataFile) { + Map<String, Property> properties = pkg.getProperties(); + if (!properties.containsKey(PROPERTY_ANDROID_SAFETY_LABEL_PATH)) { + return false; + } + Property fileInAPkPathProperty = properties.get(PROPERTY_ANDROID_SAFETY_LABEL_PATH); + if (!fileInAPkPathProperty.isString()) { + return false; + } + String fileInApkPath = fileInAPkPathProperty.getString(); + List<AndroidPackageSplit> splits = pkg.getSplits(); + for (int i = 0; i < splits.size(); i++) { + try (ZipFile zipFile = new ZipFile(splits.get(i).getPath())) { + ZipEntry zipEntry = zipFile.getEntry(fileInApkPath); + if (zipEntry != null && zipEntry.getSize() <= getAppMetadataSizeLimit()) { + try (InputStream in = zipFile.getInputStream(zipEntry)) { + try (FileOutputStream out = new FileOutputStream(appMetadataFile)) { + FileUtils.copy(in, out); + Os.chmod(appMetadataFile.getAbsolutePath(), + APP_METADATA_FILE_ACCESS_MODE); + return true; + } } - Os.chmod(appMetadataFile.getAbsolutePath(), APP_METADATA_FILE_ACCESS_MODE); - break; } + } catch (Exception e) { + Slog.e(TAG, e.getMessage()); } - } catch (Exception e) { - Slog.e(TAG, e.getMessage()); - return false; } - return found; + return false; } public static void linkFilesToOldDirs(@NonNull Installer installer, 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/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 70913c351eeb..cd1d7996fbac 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -675,6 +675,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt // TODO: switch this back to SecurityException Slog.wtf(TAG, "Not allowed to modify non-dynamic permission " + permName); + return; } mRegistry.removePermission(permName); } diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index f3d7dd19ecc2..ed9fa65dee15 100644 --- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -210,6 +210,18 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { } @Override + public void onLinkPropertiesOrCapabilitiesChanged() { + if (!isStarted()) return; + + reschedulePolling(); + } + + private void reschedulePolling() { + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L); + } + + @Override protected void start() { super.start(); clearTransformStateAndPollingEvents(); diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java index 4bacf3b8abe5..a1b212f8d3d7 100644 --- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java +++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java @@ -186,6 +186,11 @@ public abstract class NetworkMetricMonitor implements AutoCloseable { // Subclasses MUST override it if they care } + /** Called when LinkProperties or NetworkCapabilities have changed */ + public void onLinkPropertiesOrCapabilitiesChanged() { + // Subclasses MUST override it if they care + } + public boolean isValidationFailed() { return mIsValidationFailed; } diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java index 2f4cf5e5d8c7..78e06d46c74c 100644 --- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java +++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java @@ -25,6 +25,7 @@ import android.net.IpSecTransform; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.vcn.Flags; import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkTemplate; import android.os.Handler; @@ -295,6 +296,12 @@ public class UnderlyingNetworkEvaluator { updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + if (Flags.evaluateIpsecLossOnLpNcChange()) { + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.onLinkPropertiesOrCapabilitiesChanged(); + } + } } /** Set the LinkProperties */ @@ -308,6 +315,12 @@ public class UnderlyingNetworkEvaluator { updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + if (Flags.evaluateIpsecLossOnLpNcChange()) { + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.onLinkPropertiesOrCapabilitiesChanged(); + } + } } /** Set whether the network is blocked */ 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/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/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index 96e9ca0c19b5..d4b57f191ecd 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -275,7 +275,6 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isUpdatableSystem, AndroidPackage::getEmergencyInstaller, AndroidPackage::isAllowCrossUidActivitySwitchFromBelow, - PackageImpl::isAppMetadataFileInApk, ) override fun extraParams() = listOf( 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/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/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..4f7be46f95e7 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; @@ -101,7 +102,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 +111,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 +124,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 +131,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 +186,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 +224,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 +237,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 +269,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 +411,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 +468,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Mock StatusBarManagerInternal mStatusBar; + @Mock + NotificationAttentionHelper mAttentionHelper; + private NotificationManagerService.WorkerHandler mWorkerHandler; private class TestableToastCallback extends ITransientNotification.Stub { @@ -661,7 +638,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 +672,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 +693,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 +764,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 +1014,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 +1789,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, @@ -10105,13 +10066,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 +10084,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 +14711,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/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index 887e5ee0c58a..e4436966ae03 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -304,8 +304,7 @@ public class ContentRecorderTests extends WindowTestsBase { mContentRecorder.updateRecording(); // Resize the output surface. - final Point newSurfaceSize = new Point(Math.round(mSurfaceSize.x / 2f), - Math.round(mSurfaceSize.y * 2)); + final Point newSurfaceSize = new Point(Math.round(mSurfaceSize.x / 2f), mSurfaceSize.y * 2); doReturn(newSurfaceSize).when(mWm.mDisplayManagerInternal).getDisplaySurfaceDefaultSize( anyInt()); mContentRecorder.onConfigurationChanged( 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) { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 69c47d43e2b8..0a8a18dc6407 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -15013,7 +15013,8 @@ public class TelephonyManager { /** * Get the emergency assistance package name. * - * @return the package name of the emergency assistance app. + * @return the package name of the emergency assistance app, or {@code null} if no app + * supports emergency assistance. * @throws IllegalStateException if emergency assistance is not enabled or the device is * not voice capable. * diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java index 5107943c3528..fdf8fb8d3c41 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java @@ -34,6 +34,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -417,4 +418,31 @@ public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase { checkGetPacketLossRate(oldState, 20000, 14000, 4096, 19); checkGetPacketLossRate(oldState, 20000, 14000, 3000, 10); } + + // Verify the polling event is scheduled with expected delays + private void verifyPollEventDelayAndScheduleNext(long expectedDelayMs) { + if (expectedDelayMs > 0) { + mTestLooper.dispatchAll(); + verify(mIpSecTransform, never()).requestIpSecTransformState(any(), any()); + mTestLooper.moveTimeForward(expectedDelayMs); + } + + mTestLooper.dispatchAll(); + verify(mIpSecTransform).requestIpSecTransformState(any(), any()); + reset(mIpSecTransform); + } + + @Test + public void testOnLinkPropertiesOrCapabilitiesChange() throws Exception { + // Start the monitor; verify the 1st poll is scheduled without delay + startMonitorAndCaptureStateReceiver(); + verifyPollEventDelayAndScheduleNext(0 /* expectedDelayMs */); + + // Verify the 2nd poll is rescheduled without delay + mIpSecPacketLossDetector.onLinkPropertiesOrCapabilitiesChanged(); + verifyPollEventDelayAndScheduleNext(0 /* expectedDelayMs */); + + // Verify the 3rd poll is scheduled with configured delay + verifyPollEventDelayAndScheduleNext(POLL_IPSEC_STATE_INTERVAL_MS); + } } diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index 444208edc473..af6daa17e223 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -122,6 +122,7 @@ public abstract class NetworkEvaluationTestBase { MockitoAnnotations.initMocks(this); mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS); + mSetFlagsRule.enableFlags(Flags.FLAG_EVALUATE_IPSEC_LOSS_ON_LP_NC_CHANGE); when(mNetwork.getNetId()).thenReturn(-1); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java index aa81efe9a1ce..1d6872195e81 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java @@ -31,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -333,4 +334,36 @@ public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { .compare(penalized, notPenalized); assertEquals(1, result); } + + @Test + public void testNotifyNetworkMetricMonitorOnLpChange() throws Exception { + // Clear calls invoked when initializing mNetworkEvaluator + reset(mIpSecPacketLossDetector); + + final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator(); + evaluator.setNetworkCapabilities( + CELL_NETWORK_CAPABILITIES, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + + verify(mIpSecPacketLossDetector).onLinkPropertiesOrCapabilitiesChanged(); + } + + @Test + public void testNotifyNetworkMetricMonitorOnNcChange() throws Exception { + // Clear calls invoked when initializing mNetworkEvaluator + reset(mIpSecPacketLossDetector); + + final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator(); + evaluator.setLinkProperties( + LINK_PROPERTIES, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + + verify(mIpSecPacketLossDetector).onLinkPropertiesOrCapabilitiesChanged(); + } } |