Merge "In case unarchival confirmation compat option is used, open app details page during ongoing unarchival." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3391698..233fb8a 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -631,6 +631,12 @@
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 @@
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 982ab64..93fa9a0 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -57034,7 +57034,6 @@
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 0ed25eb..ff713d0 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -2685,7 +2685,8 @@
.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 7337a7c..d7b9a2c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -24,6 +24,7 @@
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 @@
/**
* 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 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 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 @@
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 @@
}
}
- 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 66ec865..103af4b 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -66,6 +66,8 @@
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.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 @@
/** @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 @@
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 @@
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 e9a7460..29ffdc5 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -71,4 +71,14 @@
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 2c26389..5e00b7a 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -1086,7 +1086,7 @@
}
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 @@
}
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 @@
@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 @@
@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 1b00f90e..57d59e5 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -59,16 +59,12 @@
int userId);
@EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
- void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId);
+ void registerDevicePresenceListenerService(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);
-
- @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 @@
@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 @@
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 41f0936..4c0da7c 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -267,6 +267,15 @@
"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 311e991..f26a797 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -98,6 +98,46 @@
}
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 1d2f653..ef50045 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 @@
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 @@
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 5f688f6..99b3f9a 100644
--- a/core/java/com/android/internal/app/IBatteryStats.aidl
+++ b/core/java/com/android/internal/app/IBatteryStats.aidl
@@ -43,14 +43,16 @@
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 e1aff2a..b316a01 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 @@
@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 @@
return memtagMode;
}
- @Override
- public boolean isAppMetadataFileInApk() {
- return mAppMetadataFileInApk;
- }
-
@Nullable
@Override
public Bundle getMetaData() {
@@ -2158,12 +2151,6 @@
}
@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 @@
dest.writeLong(this.mBooleans);
dest.writeLong(this.mBooleans2);
dest.writeBoolean(this.mAllowCrossUidActivitySwitchFromBelow);
- dest.writeBoolean(this.mAppMetadataFileInApk);
}
public PackageImpl(Parcel in) {
@@ -3445,7 +3431,6 @@
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 3c26a7c..66cfb69 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 @@
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 5ab17a6..5d185af 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 @@
@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 95ecd47..9df93f9 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.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 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 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 @@
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 b6066ba..9c63d0d 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -1297,6 +1297,17 @@
*/
@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 @@
*/
@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 65d5a1f..e5ef833 100644
--- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
+++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java
@@ -250,14 +250,6 @@
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 @@
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 @@
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 444288c..fd4ff29 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -99,6 +99,7 @@
"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 927c67c..be0669c 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.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.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.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 @@
@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 @@
mOriginalWindowTokenClientController = WindowTokenClientController.getInstance();
mOriginalAppConfig = new Configuration(ActivityThread.currentActivityThread()
.getConfiguration());
+ mActivityWindowInfoListener = spy(new ActivityWindowInfoListener());
}
@After
@@ -808,96 +807,107 @@
@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());
+ // 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));
- 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();
+ // The same ActivityWindowInfo won't trigger duplicated callback.
+ verify(mActivityWindowInfoListener, never()).accept(activityClientRecord.token,
+ activityClientRecord.getActivityWindowInfo());
- verify(mActivityWindowInfoListener).accept(activityClientRecord.token,
- activityWindowInfo);
+ // 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();
+ 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);
- verify(mActivityWindowInfoListener).accept(activity.getActivityToken(),
- activityWindowInfo);
+ clearInvocations(mActivityWindowInfoListener);
+ activityThread.executeTransaction(transaction);
- 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();
+ // Trigger callback with a different ActivityWindowInfo
+ verify(mActivityWindowInfoListener).accept(activity.getActivityToken(),
+ activityWindowInfo);
- // The same ActivityWindowInfo won't trigger duplicated callback.
- verify(mActivityWindowInfoListener, never()).accept(any(), any());
+ // 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 @@
@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 @@
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 faad472..365f3bf 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_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 @@
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 @@
@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 @@
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 3b2ab4c..2c3ee34 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 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 @@
}
};
- 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 cae232e..b8ac191 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.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.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.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 @@
@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 @@
* 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 @@
* 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 @@
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 @@
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 @@
*
* 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 @@
mDecorSurfaceOwner = container.getTaskFragmentToken();
}
+ @GuardedBy("mLock")
private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
if (mDecorSurfaceOwner != null) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
@@ -215,6 +264,7 @@
removeDivider();
}
+ @GuardedBy("mLock")
private void removeDivider() {
if (mRenderer != null) {
mRenderer.release();
@@ -238,7 +288,7 @@
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 @@
}
}
- 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 @@
.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 @@
private final boolean mIsVerticalSplit;
private final int mDisplayId;
+ private final boolean mIsReversedLayout;
@VisibleForTesting
Properties(
@@ -419,12 +685,14 @@
@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 @@
&& 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 @@
* 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 @@
private final SurfaceControlViewHost mViewHost;
@NonNull
private final FrameLayout mDividerLayout;
- private final int mDividerWidthPx;
+ @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() {
- mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+ 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 @@
}
/** 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);
- }
-
- private void updateSurface() {
- 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.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
- } else {
- t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition);
- t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
- }
+ t.remove(mDividerSurface);
+ removeVeils(t);
t.apply();
}
+ 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();
+ if (mProperties.mIsVerticalSplit) {
+ t.setPosition(mDividerSurface, mDividerPosition, 0.0f);
+ t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
+ } else {
+ t.setPosition(mDividerSurface, 0.0f, mDividerPosition);
+ t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
+ }
+ 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 @@
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 @@
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 @@
button.setImageDrawable(rotatedHandle);
}
+
+ button.setOnTouchListener(mListener);
mDividerLayout.addView(button);
}
@@ -613,5 +931,69 @@
.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 3f4dddf..32f2d67 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 @@
/**
* 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 @@
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 0000000..042a68a6
--- /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 1bc8264..b9b86f0 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 @@
* 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 @@
@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 @@
: 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 @@
// 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 @@
if (taskContainer.isEmpty()) {
// Cleanup the TaskContainer if it becomes empty.
mTaskContainers.remove(taskContainer.getTaskId());
+ mDividerPresenters.remove(taskContainer.getTaskId());
}
return;
}
@@ -1759,6 +1769,7 @@
}
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 @@
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 20bc820..0d31266 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 @@
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.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 @@
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 @@
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 @@
*/
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 @@
// 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 e75a317..a215bdf 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 @@
*/
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 @@
// 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 @@
mTaskFragmentParentInfo = info;
}
+ @Nullable
+ TaskFragmentParentInfo getTaskFragmentParentInfo() {
+ return mTaskFragmentParentInfo;
+ }
+
/**
* Returns {@code true} if the container should be updated with {@code info}.
*/
@@ -398,16 +398,22 @@
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 4d1d807..47d01da 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.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.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.concurrent.Executor;
+
/**
* Test class for {@link DividerPresenter}.
*
@@ -73,6 +76,8 @@
@Rule
public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+ private static final int MOCK_TASK_ID = 1234;
+
@Mock
private DividerPresenter.Renderer mRenderer;
@@ -83,6 +88,12 @@
private TaskFragmentParentInfo mParentInfo;
@Mock
+ private TaskContainer mTaskContainer;
+
+ @Mock
+ private DividerPresenter.DragEventCallback mDragEventCallback;
+
+ @Mock
private SplitContainer mSplitContainer;
@Mock
@@ -110,6 +121,8 @@
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 @@
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 @@
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 014b841..341c3e8c 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -345,6 +345,7 @@
"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 @@
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 @@
"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 @@
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 0780414..8645995 100644
--- a/libs/hwui/AnimatorManager.cpp
+++ b/libs/hwui/AnimatorManager.cpp
@@ -90,7 +90,13 @@
}
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 27773a6..69613c7 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 @@
} 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 @@
}
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 825dd4c..e39c8d5 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() {
+#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 90b1da8..0f80c55 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 @@
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 @@
};
class JniAnimationEndListener : public OnAnimationEndListener {
+#ifdef __ANDROID__
public:
JniAnimationEndListener(sp<Looper>&& looper, JNIEnv* env, jobject javaObject) {
mListener = new InvokeListener(env, javaObject);
@@ -215,6 +235,17 @@
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 @@
if (!jdrawable) {
drawable->setOnAnimationEndListener(nullptr);
} else {
+#ifdef __ANDROID__
sp<Looper> looper = Looper::getForThread();
if (!looper.get()) {
doThrowISE(env,
@@ -233,6 +265,10 @@
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 283dc7d..0fe35e6 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 @@
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 9d97763..b7fa33e 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.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 @@
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 @@
}
}
-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 2e80a7c..c05fc93 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 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 @@
*/
@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 bf4c988..018db68 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.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 @@
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun WearApp(
- viewModel: CredentialSelectorViewModel,
- flowEngine: FlowEngine = viewModel,
+ flowEngine: FlowEngine,
onCloseApp: () -> Unit,
) {
val navController = rememberSwipeDismissableNavController()
@@ -62,7 +60,7 @@
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 @@
}
}
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 c755623..4147813 100644
--- a/packages/SettingsLib/Spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/build.gradle.kts
@@ -29,7 +29,7 @@
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
similarity index 87%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPage.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/LoadingBarPageProvider.kt
index 247990c..f1cbc37 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.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 @@
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 ff2a1e8..0ee9d59 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 f2b9235..2f2ac24 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -57,13 +57,13 @@
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 0e7e499..2de73c0 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 @@
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 @@
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 354b95d..f372a45 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 @@
data class BottomAppBarButton(
val text: String,
- val onClick: () -> Unit,
+ val enabled: Boolean = true,
+ val onClick: () -> Unit
)
@Composable
@@ -122,13 +123,13 @@
) {
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 1741f13..be178ff 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.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 f36da19..20f1b17 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -144,7 +144,8 @@
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 0c54c19..45754eb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java
@@ -289,7 +289,8 @@
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 a12f099..acd9e3d 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 @@
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 2781f39..1c938a6 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.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
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 @@
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 @@
(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 d72d5ca..b4472fc 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.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 @@
) {
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 @@
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 @@
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 @@
}
}
- scene(ClockScenes.splitShadeSmallClockScene) {
+ scene(splitShadeSmallClockScene) {
Row(
modifier = Modifier.fillMaxSize(),
) {
@@ -133,7 +165,7 @@
SmallClock(
burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmallClockTopChanged,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.wrapContentSize()
)
}
with(smartSpaceSection) {
@@ -155,13 +187,13 @@
}
}
- 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 @@
}
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 @@
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 cea49e1..11c9462 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 @@
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 @@
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 54c7a08..b392014 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 @@
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 @@
}
override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
}
inner class LargeClockAnimations(
@@ -326,6 +332,10 @@
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 f8321b7..07e9815 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.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 @@
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 b253309..d88260f 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.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.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
@SmallTest
@@ -81,10 +86,19 @@
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 fd7a7f3..8e2bd9b 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 @@
* 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 41fb57a..cf9ca15 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 c998535..221b791 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
new file mode 100644
index 0000000..a38b23b
--- /dev/null
+++ b/packages/SystemUI/res/raw/widget.rec
Binary files differ
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 9f13e6d..e172cad 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 @@
// 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 @@
// 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 5e34d29..e1ae498 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 @@
/**
* 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 @@
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 @@
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 @@
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 307b985..20e81c2 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.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 @@
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 7106674..2797b7b 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 @@
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 000f03a..7525ce0 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.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 @@
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 @@
} 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 @@
}
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 e298154..1705909 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.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 @@
) {
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 d9d7470..fa845c7 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.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.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 @@
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 @@
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 @@
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 a293afc..182798e 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.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 @@
// 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 c749818..a861a87 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 @@
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 3a2781c..4cb342b 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.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 @@
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 @@
}
}
- 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 0ed42ef..4d9354dd 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.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 @@
deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
vibratorHelper: VibratorHelper?,
falsingManager: FalsingManager?,
+ keyguardViewMediator: KeyguardViewMediator?,
): DisposableHandle {
var onLayoutChangeListener: OnLayoutChange? = null
val childViews = mutableMapOf<Int, View>()
@@ -298,8 +300,12 @@
}
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 14ab17f..cbf52ef 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 @@
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 065c20a..b432417 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 @@
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 @@
@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 ce45112..ded680c 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.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 @@
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 10a9e3b..06a0c72 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 @@
)
/** 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 0000000..7814576
--- /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 1c1c33a..3d64951 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 @@
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 0a880293..9e31379 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_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.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.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 @@
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 e1c543f..2360f27 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 @@
/** 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 @@
// 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 e8e629c..59da8f1 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.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.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 @@
QuickSettingsController quickSettingsController,
PrimaryBouncerInteractor primaryBouncerInteractor,
AlternateBouncerInteractor alternateBouncerInteractor,
- Lazy<JavaAdapter> javaAdapter,
- Lazy<AlternateBouncerDependencies> alternateBouncerDependencies,
BouncerViewBinder bouncerViewBinder) {
mLockscreenShadeTransitionController = transitionController;
mFalsingCollector = falsingCollector;
@@ -224,23 +217,6 @@
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 9963f81..5b2377f 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 @@
val shadeHeadsUpTracker: ShadeHeadsUpTracker
/** Returns the ShadeFoldAnimator. */
+ @Deprecated("This interface is deprecated in Scene Container")
val shadeFoldAnimator: ShadeFoldAnimator
companion object {
@@ -142,9 +143,10 @@
}
/** 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 @@
* @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 @@
* 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 fb52838..bef26d9 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 @@
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 @@
@Override
public WindowInsets onProgress(WindowInsets windowInsets,
List<WindowInsetsAnimation> list) {
- updateBottomInset(windowInsets);
+ updateImeInset(windowInsets);
return windowInsets;
}
@@ -1792,8 +1793,8 @@
+ ((!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 @@
}
if (!mIsInsetAnimationRunning) {
// update bottom inset e.g. after rotation
- updateBottomInset(insets);
+ updateImeInset(insets);
}
return insets;
}
@@ -2218,9 +2219,9 @@
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 a99834a..febe5a2 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 @@
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 @@
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 @@
mPrimaryBouncerInteractor.getKeyguardAuthenticatedBiometricsHandled(),
this::consumeKeyguardAuthenticatedBiometricsHandled
);
+ } else {
+ mListenForCanShowAlternateBouncer = mJavaAdapter.alwaysCollectFlow(
+ mAlternateBouncerInteractor.getCanShowAlternateBouncer(),
+ this::consumeCanShowAlternateBouncer
+ );
}
if (KeyguardWmStateRefactor.isEnabled()) {
@@ -558,6 +568,11 @@
}
}
+ 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 @@
* {@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 @@
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 098d51e..81c8d50 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.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.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 @@
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 @@
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 @@
} 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 @@
// 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 @@
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 5509c04..d4a0c8f 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.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.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 @@
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 2014755..ae20c70 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.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.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 @@
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 cb8c40c..3b4f683 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.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 @@
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 0000000..143c4da
--- /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 e53cd11..d12980a 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 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 @@
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 db0c0bc..890e1e0 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.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 @@
@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 @@
@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 @@
@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 @@
@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 @@
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 04e214a..2b1ac91 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 @@
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 88b239a..b699613 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.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.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.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 @@
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 59fe813..2ecca2e 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.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.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 @@
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 6f16d65..811e9bf 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.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 @@
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 @@
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 13df091..abb9432 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 @@
@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 10aab96..cb7d276 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 @@
@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 @@
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 @@
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 @@
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 c4fc30d..070a369 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 @@
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 @@
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 5f5d428..7eef704 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.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 @@
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 0000000..92cfbef
--- /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 880a687..a57138f 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 @@
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 16fe007..cfeb5f4 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.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.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 @@
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 @@
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 @@
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 d006cf6..993a1d5 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 @@
mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_FAILURE);
}
mPresentationStatsEventLogger.logAndEndEvent();
+ mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis();
mFillResponseEventLogger.logAndEndEvent();
}
notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED,
@@ -5400,6 +5401,7 @@
}
// 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 0000000..0a41485
--- /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 f4f6c13..712162b 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 @@
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.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.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.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.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.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.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 @@
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 @@
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 @@
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 @@
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 @@
mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor,
mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager);
- mInactiveAssociationsRemovalService = new InactiveAssociationsRemovalService(
- mAssociationStore, mDisassociationProcessor);
-
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
mPackageManagerInternal, mAssociationStore,
mSystemDataTransferRequestStore, mTransportManager);
@@ -289,6 +302,181 @@
}
}
+ @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 @@
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 @@
@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 @@
}
@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 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 void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
String packageName, int userId) {
stopObservingDevicePresence_enforcePermission();
+ registerDevicePresenceListener(request, packageName, userId, /* active */ false);
+ }
- mDevicePresenceMonitor.stopObservingDevicePresence(request, packageName, userId);
+ private void registerDevicePresenceListener(ObservingDevicePresenceRequest request,
+ String packageName, int userId, boolean active) {
+ enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
+ enforceCallerIsSystemOr(userId, packageName);
+
+ final int associationId = request.getAssociationId();
+ final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
+ associationId);
+ final ParcelUuid uuid = request.getUuid();
+
+ if (uuid != null) {
+ enforceCallerCanObservingDevicePresenceByUuid(getContext());
+ if (active) {
+ startObservingDevicePresenceByUuid(uuid, packageName, userId);
+ } else {
+ stopObservingDevicePresenceByUuid(uuid, packageName, userId);
+ }
+ } else if (associationInfo == null) {
+ throw new IllegalArgumentException("App " + packageName
+ + " is not associated with device " + request.getAssociationId()
+ + " for user " + userId);
+ } else {
+ processDevicePresenceListener(
+ associationInfo, userId, packageName, active);
+ }
+ }
+
+ private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+ int userId) {
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+ for (ObservableUuid observableUuid : observableUuids) {
+ if (observableUuid.getUuid().equals(uuid)) {
+ Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName
+ + "has been already scheduled for observing");
+ return;
+ }
+ }
+
+ final ObservableUuid observableUuid = new ObservableUuid(userId, uuid,
+ packageName, System.currentTimeMillis());
+
+ mObservableUuidStore.writeObservableUuid(userId, observableUuid);
+ }
+
+ private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+ int userId) {
+ final List<ObservableUuid> uuidsTobeObserved =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+ boolean isScheduledObserving = false;
+
+ for (ObservableUuid observableUuid : uuidsTobeObserved) {
+ if (observableUuid.getUuid().equals(uuid)) {
+ isScheduledObserving = true;
+ break;
+ }
+ }
+
+ if (!isScheduledObserving) {
+ Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName
+ + "has NOT been scheduled for observing yet");
+ return;
+ }
+
+ mObservableUuidStore.removeObservableUuid(userId, uuid, packageName);
+ mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid);
+
+ if (!shouldBindPackage(userId, packageName)) {
+ mCompanionAppController.unbindCompanionApplication(userId, packageName);
+ }
}
@Override
@@ -591,7 +874,8 @@
@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 @@
ParcelFileDescriptor fd) {
attachSystemDataTransport_enforcePermission();
- mTransportManager.attachSystemDataTransport(associationId, fd);
+ getAssociationWithCallerChecks(associationId);
+ mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd);
}
@Override
@@ -615,56 +900,96 @@
public void detachSystemDataTransport(String packageName, int userId, int associationId) {
detachSystemDataTransport_enforcePermission();
- mTransportManager.detachSystemDataTransport(associationId);
+ 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(MANAGE_COMPANION_DEVICES)
public void enableSecureTransport(boolean enabled) {
enableSecureTransport_enforcePermission();
-
mTransportManager.enableSecureTransport(enabled);
}
@Override
- public void enableSystemDataSync(int associationId, int flags) {
- mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags);
+ 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
- public void disableSystemDataSync(int associationId, int flags) {
- mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags);
- }
+ public void notifyDeviceDisappeared(int associationId) {
+ if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId);
- @Override
- public void enablePermissionsSync(int associationId) {
- mSystemDataTransferProcessor.enablePermissionsSync(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.");
+ }
- @Override
- public void disablePermissionsSync(int associationId) {
- mSystemDataTransferProcessor.disablePermissionsSync(associationId);
- }
+ mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
- @Override
- public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
- return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
- }
-
- @Override
- @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
- public void notifySelfManagedDeviceAppeared(int associationId) {
- notifySelfManagedDeviceAppeared_enforcePermission();
-
- mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, true);
- }
-
- @Override
- @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
- public void notifySelfManagedDeviceDisappeared(int associationId) {
- notifySelfManagedDeviceDisappeared_enforcePermission();
-
- 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 @@
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 @@
}
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 @@
@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 @@
}
}
+ 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 @@
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 @@
}
};
+ 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 @@
}
@Override
- public void onPackageModified(@NonNull String packageName) {
+ public void onPackageModified(String packageName) {
onPackageModifiedInternal(getChangingUserId(), packageName);
}
@@ -906,12 +1325,28 @@
}
};
+ 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 e3b4c95..cdf832f 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 @@
*/
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
similarity index 83%
rename from services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
rename to services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
index c01c319..5abdb42 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.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 @@
* 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 @@
return mIsPrimary;
}
- @NonNull
ComponentName getComponentName() {
return mComponentName;
}
@@ -147,15 +140,17 @@
@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 @@
* 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 @@
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 @@
* <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 a789384..a7a73cb 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 @@
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.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 @@
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 @@
CompanionDeviceShellCommand(CompanionDeviceManagerService service,
AssociationStore associationStore,
- DevicePresenceProcessor devicePresenceProcessor,
+ CompanionDevicePresenceMonitor devicePresenceMonitor,
CompanionTransportManager transportManager,
SystemDataTransferProcessor systemDataTransferProcessor,
AssociationRequestsProcessor associationRequestsProcessor,
@@ -66,7 +68,7 @@
DisassociationProcessor disassociationProcessor) {
mService = service;
mAssociationStore = associationStore;
- mDevicePresenceProcessor = devicePresenceProcessor;
+ mDevicePresenceMonitor = devicePresenceMonitor;
mTransportManager = transportManager;
mSystemDataTransferProcessor = systemDataTransferProcessor;
mAssociationRequestsProcessor = associationRequestsProcessor;
@@ -83,7 +85,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 a18776e..a02d9f9 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 @@
/**
* 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 @@
// 2b.4. Send the PendingIntent back to the app.
try {
callback.onAssociationPending(pendingIntent);
- } catch (RemoteException ignore) {
- }
+ } catch (RemoteException ignore) { }
}
/**
@@ -254,8 +252,7 @@
// 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 @@
* 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 @@
* 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 @@
// 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 ae2b708..edebb55 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 @@
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.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 @@
@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 @@
/**
* 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 @@
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 @@
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 @@
}
/**
- * 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 20de121..ec897791 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.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 @@
@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 @@
@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 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 @@
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 @@
}
}
- /**
- * @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 @@
() -> 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 b52904a..f287315 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.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 @@
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 @@
.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 9069689..c5ca0bf 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.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.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 @@
}
/**
+ * 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 @@
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 @@
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 @@
* 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 @@
*/
@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 9c37881..c89ce11 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_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 2d345c4..cb363a7 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 @@
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 4ba4e2c..0000000
--- 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 0000000..7a1a83f
--- /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 2a933a8..0000000
--- 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 fa0f6bd..db15da29 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 @@
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 697ef87..793fb7f 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 @@
@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 @@
}
}
- /**
- * 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 @@
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 @@
transport.stop();
notifyOnTransportsChanged();
}
-
- Slog.i(TAG, "Transport detached.");
}
private void notifyOnTransportsChanged() {
@@ -323,7 +307,8 @@
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 81dc36d..254d28b 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 @@
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 d7e766e..2cf1f46 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.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 @@
/**
* 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 @@
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 0f951746..649b9ef 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 @@
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 9950d8f..da26209 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 @@
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 @@
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 @@
}
/**
+ *
+ * @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 0807cc0..d99f712 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 @@
&& (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 d8a4500..9c7504d 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 @@
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 @@
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 @@
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 @@
}
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 a91bb59..7f88c30 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 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 @@
// '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 @@
mHandler.post(() -> {
start();
});
+
+ mDisplayDeviceConfig = displayDeviceConfig;
}
/**
@@ -78,41 +86,45 @@
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;
+ /* def= */ 0, userId) == 1.0f && mAutoBrightnessEnabled;
- // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux);
- float luxBasedNitsLowerBound = 2.0f;
+ float luxBasedNitsLowerBound = mDisplayDeviceConfig.getMinNitsFromLux(mAmbientLux);
- final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
- luxBasedNitsLowerBound) : MIN_NITS;
+ final int reason;
+ float minNitsAllowed = -1f; // undefined, if setting is off.
+ final float minBrightnessAllowed;
- final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
- ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
- : BrightnessReason.MODIFIER_MIN_LUX;
+ 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;
+ }
- // TODO: brightnessLowerBound = nitsToBrightnessSpline(nitsLowerBound);
- final float brightnessLowerBound = PowerManager.BRIGHTNESS_MIN;
-
- 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 @@
}
@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 2a3dd87..db5a524d 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 @@
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 2234258..1606159c 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 @@
* @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 aa82533..1a4e807 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 @@
* 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 @@
mNitsToBacklight = nitsToBacklight;
mBrightnessToBacklight = brightnessToBacklight;
mBacklightToBrightness = backlightToBrightness;
+ mMinLuxToNits = minLuxToNits;
}
@Override
@@ -92,6 +95,7 @@
+ ", mNitsToBacklight: " + mNitsToBacklight
+ ", mBrightnessToBacklight: " + mBrightnessToBacklight
+ ", mBacklightToBrightness: " + mBacklightToBrightness
+ + ", mMinLuxToNits: " + mMinLuxToNits
+ "} ";
}
@@ -132,11 +136,40 @@
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 a5f241f..49a8553 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 {
+ 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 dd6433d..c7b60da 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 @@
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 @@
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 @@
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 99c7397..a100fe0 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 @@
@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 @@
}
/**
- * 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 @@
}
/**
+ * 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 @@
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 c406e17..b595f0e 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 @@
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 @@
@MultiUserUnawareField
private final InputMethodMenuController mMenuController;
@MultiUserUnawareField
+ @NonNull private final InputMethodBindingController mBindingController;
+ @MultiUserUnawareField
@NonNull private final AutofillSuggestionsController mAutofillController;
@GuardedBy("ImfLock.class")
@@ -463,14 +462,12 @@
@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 @@
*/
@GuardedBy("ImfLock.class")
private int getSequenceNumberLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mSequence.getSequenceNumber();
+ return mBindingController.getSequenceNumber();
}
/**
@@ -489,14 +485,13 @@
*/
@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 @@
@GuardedBy("ImfLock.class")
@Nullable
private String getCurIdLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getCurId();
+ return mBindingController.getCurId();
}
/**
@@ -575,8 +569,7 @@
*/
@GuardedBy("ImfLock.class")
private boolean hasConnectionLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.hasMainConnection();
+ return mBindingController.hasMainConnection();
}
/**
@@ -599,8 +592,7 @@
@GuardedBy("ImfLock.class")
@Nullable
private Intent getCurIntentLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getCurIntent();
+ return mBindingController.getCurIntent();
}
/**
@@ -610,8 +602,7 @@
@GuardedBy("ImfLock.class")
@Nullable
IBinder getCurTokenLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getCurToken();
+ return mBindingController.getCurToken();
}
/**
@@ -652,8 +643,7 @@
@GuardedBy("ImfLock.class")
@Nullable
IInputMethodInvoker getCurMethodLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getCurMethod();
+ return mBindingController.getCurMethod();
}
/**
@@ -661,8 +651,7 @@
*/
@GuardedBy("ImfLock.class")
private int getCurMethodUidLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getCurMethodUid();
+ return mBindingController.getCurMethodUid();
}
/**
@@ -671,8 +660,7 @@
*/
@GuardedBy("ImfLock.class")
private long getLastBindTimeLocked() {
- final UserData userData = UserData.getOrCreate(mCurrentUserId);
- return userData.mBindingController.getLastBindTime();
+ return mBindingController.getLastBindTime();
}
/**
@@ -824,8 +812,7 @@
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 @@
// 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 @@
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 @@
@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 @@
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 @@
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 @@
AdditionalSubtypeMapRepository.putAndSave(userId, newAdditionalSubtypeMap,
settings.getMethodMap());
}
- if (isCurrentUser
- && !(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) {
- return;
- }
- final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
- userId, newAdditionalSubtypeMap, DirectBootAwareness.AUTO);
- InputMethodSettingsRepository.put(userId, newSettings);
if (!isCurrentUser) {
return;
}
+
+ if (!(additionalSubtypeChanged || shouldRebuildInputMethodListLocked())) {
+ return;
+ }
+ mSettings = queryInputMethodServicesInternal(mContext, userId,
+ newAdditionalSubtypeMap, DirectBootAwareness.AUTO);
postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */);
boolean changed = false;
@@ -1306,20 +1289,21 @@
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 (userId != currentUserId) {
+ return;
}
if (!mSystemReady) {
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 */);
- }
+ mSettings = queryInputMethodServicesInternal(mContext, userId,
+ AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
+ // We need to rebuild IMEs.
+ postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */);
+ updateInputMethodsFromSettingsLocked(true /* enabledChanged */);
}
}
@@ -1383,32 +1367,33 @@
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 @@
@GuardedBy("ImfLock.class")
@UserIdInt
int getCurrentImeUserIdLocked() {
- return mCurrentUserId;
+ return mSettings.getUserId();
}
private final class InkWindowInitializer implements Runnable {
@@ -1463,13 +1448,12 @@
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 @@
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 @@
// 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 @@
// 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 @@
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 @@
}
if (!mSystemReady) {
mSystemReady = true;
- final int currentUserId = mCurrentUserId;
+ final int currentUserId = mSettings.getUserId();
mStatusBarManagerInternal =
LocalServices.getService(StatusBarManagerInternal.class);
hideStatusBarIconLocked();
@@ -1614,7 +1598,7 @@
// 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 @@
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 @@
}
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 @@
}
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 @@
}
// 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 @@
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 @@
@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 @@
@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 @@
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 @@
// 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 @@
}
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 @@
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 @@
}
}
- userData.mBindingController.unbindCurrentMethod();
- return userData.mBindingController.bindCurrentMethod();
+ mBindingController.unbindCurrentMethod();
+
+ return mBindingController.bindCurrentMethod();
}
/**
@@ -2253,18 +2256,17 @@
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 @@
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 @@
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 @@
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 @@
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 @@
} 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 @@
return false;
}
if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
- && mWindowManagerInternal.isKeyguardSecure(mCurrentUserId)) {
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId())) {
return false;
}
if ((visibility & InputMethodService.IME_ACTIVE) == 0
@@ -2739,8 +2739,7 @@
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 @@
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 @@
@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 @@
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 @@
// 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 @@
}
// 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 @@
@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 @@
// 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 @@
@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 @@
+ " 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 @@
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 @@
} 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 @@
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 @@
}
// 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 @@
// 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 @@
&& 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 @@
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 @@
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 @@
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 @@
// 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 @@
&& !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 @@
@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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
@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 @@
// 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 @@
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 @@
*/
@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 @@
// 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 @@
@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 @@
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 @@
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 @@
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 @@
*/
@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 @@
@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 @@
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 @@
@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 @@
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 @@
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 @@
? 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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
}
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 60b9a4c..0000000
--- 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 05e31ce..0000000
--- 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 8e20611..0000000
--- 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 1dc86f2..0cd7654 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 @@
}
@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 5b3934e..ce31ac8 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.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.Collection;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -639,6 +641,15 @@
}
@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 e4b2fad..0999199 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 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 e2163c5..53c32cf 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.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.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.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -136,9 +140,9 @@
/**
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 097daf2..5563cae 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 @@
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 9fcdfdd..ca3db2c 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.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.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 @@
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 @@
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 @@
// 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 @@
RankingHelper mRankingHelper;
@VisibleForTesting
PreferencesHelper mPreferencesHelper;
- private VibratorHelper mVibratorHelper;
private final UserProfiles mUserProfiles = new UserProfiles();
private NotificationListeners mListeners;
@@ -751,8 +720,6 @@
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 @@
@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 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 @@
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 @@
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 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 @@
= 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 @@
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 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 @@
// 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 @@
}
@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 @@
return mNotificationsByKey.get(key);
}
-
- @VisibleForTesting
- void setSystemReady(boolean systemReady) {
- mSystemReady = systemReady;
- }
-
@VisibleForTesting
void setHandler(WorkerHandler handler) {
mHandler = handler;
@@ -2471,13 +2290,8 @@
}
@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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- @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 @@
|| 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 @@
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 @@
}
}
- 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 @@
cancellationElapsedTimeMs);
}
}
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.updateLightsLocked();
- } else {
- updateLightsLocked();
- }
+ mAttentionHelper.updateLightsLocked();
}
}
@@ -10745,37 +9962,6 @@
}
@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 @@
}
}
- private void updateNotificationPulse() {
- synchronized (mNotificationLock) {
- updateLightsLocked();
- }
- }
-
protected boolean isCallingUidSystem() {
final int uid = Binder.getCallingUid();
return uid == Process.SYSTEM_UID;
@@ -11350,18 +10530,6 @@
}
}
- 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 6ab4b99..7e58d0a 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.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 @@
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 @@
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 c559892..e394482 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_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_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 @@
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 20b669b..4c95e83 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.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 @@
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 @@
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 @@
return true;
}
+ if (isAccessToHiddenProfilesForNonSystemAppsForbidden()) {
+ return false;
+ }
+
if (!mRoleManager
.getRoleHoldersAsUser(
RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
@@ -570,7 +576,6 @@
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 @@
}
}
+ 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 @@
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 9a2b98f..095a233 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 @@
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 @@
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 @@
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 189a138..110a29c 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 @@
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.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.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.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.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 @@
/**
* 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;
- }
- Os.chmod(appMetadataFile.getAbsolutePath(), APP_METADATA_FILE_ACCESS_MODE);
- break;
- }
- }
- } catch (Exception e) {
- Slog.e(TAG, e.getMessage());
+ public static boolean extractAppMetadataFromApk(AndroidPackage pkg, File appMetadataFile) {
+ Map<String, Property> properties = pkg.getProperties();
+ if (!properties.containsKey(PROPERTY_ANDROID_SAFETY_LABEL_PATH)) {
return false;
}
- return found;
+ 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;
+ }
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, e.getMessage());
+ }
+ }
+ 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 b18503d..1c70af0 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 @@
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 @@
// 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 @@
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 @@
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 70913c3..cd1d799 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 @@
// 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 f3d7dd1..ed9fa65 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 @@
}
@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 4bacf3b..a1b212f 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 @@
// 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 2f4cf5e..78e06d4 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.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 @@
updatePriorityClass(
underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig);
+
+ if (Flags.evaluateIpsecLossOnLpNcChange()) {
+ for (NetworkMetricMonitor monitor : mMetricMonitors) {
+ monitor.onLinkPropertiesOrCapabilitiesChanged();
+ }
+ }
}
/** Set the LinkProperties */
@@ -308,6 +315,12 @@
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 18d2718..9b2ca39 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 @@
// 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 @@
+ 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 @@
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 @@
if (inPinnedWindowingMode()) {
mTaskSupervisor.removeRootTask(this);
- return true;
+ return;
}
mRootWindowContainer.ensureVisibilityAndConfig(null /* starting */,
@@ -5896,7 +5905,6 @@
} 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 1f54518..912ff4a 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 c39c3d7..3c70890 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 @@
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 96e9ca0..d4b57f1 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 @@
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 73a2f65..5a022c0 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 @@
+ " <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 5294943..5487bc5 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.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 @@
@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 e4a7d98..749c400 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 @@
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() {
- 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)
- modifier.recalculateLowerBound()
- testHandler.flush()
- assertThat(modifier.isActive).isTrue()
-
- // TODO: code currently returns MIN/MAX; update with lux values
- assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
- }
-
- @Test
- fun testGetReason_UserSet() {
- Settings.Secure.putIntForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
- Settings.Secure.putFloatForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId)
- modifier.recalculateLowerBound()
- testHandler.flush()
- assertThat(modifier.isActive).isTrue()
-
- // Test restriction from user setting
- assertThat(modifier.brightnessReason)
- .isEqualTo(BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND)
- }
-
- @Test
- fun testGetReason_Lux() {
- Settings.Secure.putIntForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
- Settings.Secure.putFloatForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
- modifier.onAmbientLuxChange(3000.0f)
- testHandler.flush()
- assertThat(modifier.isActive).isTrue()
-
- // Test restriction from lux setting
- assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
- }
-
- @Test
fun testSettingOffDisablesModifier() {
+ // test transition point ensures brightness doesn't drop when setting is off.
Settings.Secure.putIntForUser(context.contentResolver,
Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId)
- assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+ modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED)
+ modifier.recalculateLowerBound()
+ testHandler.flush()
+ 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(PowerManager.BRIGHTNESS_MIN)
+ assertThat(modifier.brightnessLowerBound).isEqualTo(TRANSITION_POINT)
+ assertThat(modifier.brightnessReason).isEqualTo(0) // no reason - ie off
+ }
+
+ @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
+ @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.putFloatForUser(context.contentResolver,
+ Settings.Secure.EVEN_DIMMER_MIN_NITS, 10.0f, userId)
+ modifier.setAutoBrightnessState(AUTO_BRIGHTNESS_ENABLED)
+ modifier.recalculateLowerBound()
+ testHandler.flush()
+
+ // 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
+ @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) // 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)
+
+ 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
+ @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, 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()
+ // 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 517dcb4..0000000
--- 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 a1c24f1..acac63c 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 @@
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 @@
}
}
+ @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 06a4ac9..4f7be46 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.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.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.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.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.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.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.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.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.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 @@
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 @@
@Mock
StatusBarManagerInternal mStatusBar;
+ @Mock
+ NotificationAttentionHelper mAttentionHelper;
+
private NotificationManagerService.WorkerHandler mWorkerHandler;
private class TestableToastCallback extends ITransientNotification.Stub {
@@ -661,7 +638,7 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 @@
@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 @@
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 @@
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 70910b1..c1bb3e7 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 @@
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 0b76154..19ce217 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 @@
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 5861d88..185677f 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 @@
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 887e5ee..e443696 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 @@
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 ff7129c..0186006 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.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 @@
assertTrue(transitA.isCollecting());
// finish collecting A
- transitA.start();
- transitA.setAllReady();
- mSyncEngine.tryFinishForTest(transitA.getSyncId());
+ tryFinishTransitionSyncSet(transitA);
waitUntilHandlersIdle();
assertTrue(transitA.isPlaying());
@@ -2476,6 +2476,36 @@
}
@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 @@
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 @@
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 69c47d43..0a8a18dc 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -15013,7 +15013,8 @@
/**
* 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 5107943..fdf8fb8 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.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 @@
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 444208e..af6daa1 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 @@
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 aa81efe..1d68721 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.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 @@
.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();
+ }
}