Merge "Remove legacy memory trim code" into main
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index fa9346e..4bf8879 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -607,9 +607,9 @@
Configuration createdConfig;
Configuration overrideConfig;
@NonNull
- private ActivityWindowInfo mActivityWindowInfo;
- @Nullable
- private ActivityWindowInfo mLastReportedActivityWindowInfo;
+ private final ActivityWindowInfo mActivityWindowInfo = new ActivityWindowInfo();
+ @NonNull
+ private final ActivityWindowInfo mLastReportedActivityWindowInfo = new ActivityWindowInfo();
// Used for consolidating configs before sending on to Activity.
private final Configuration tmpConfig = new Configuration();
@@ -700,7 +700,7 @@
mSceneTransitionInfo = sceneTransitionInfo;
mLaunchedFromBubble = launchedFromBubble;
mTaskFragmentToken = taskFragmentToken;
- mActivityWindowInfo = activityWindowInfo;
+ mActivityWindowInfo.set(activityWindowInfo);
init();
}
@@ -720,7 +720,7 @@
throw new IllegalStateException(
"Received config update for non-existing activity");
}
- if (activityWindowInfoFlag() && activityWindowInfo == null) {
+ if (activityWindowInfo == null) {
Log.w(TAG, "Received empty ActivityWindowInfo update for r=" + activity);
activityWindowInfo = mActivityWindowInfo;
}
@@ -6063,7 +6063,7 @@
target.createdConfig = config.getGlobalConfiguration();
target.overrideConfig = config.getOverrideConfiguration();
target.pendingConfigChanges |= configChanges;
- target.mActivityWindowInfo = activityWindowInfo;
+ target.mActivityWindowInfo.set(activityWindowInfo);
}
return scheduleRelaunch ? target : null;
@@ -6257,7 +6257,7 @@
}
r.startsNotResumed = startsNotResumed;
r.overrideConfig = overrideConfig;
- r.mActivityWindowInfo = activityWindowInfo;
+ r.mActivityWindowInfo.set(activityWindowInfo);
handleLaunchActivity(r, pendingActions, mLastReportedDeviceId, customIntent);
}
@@ -6759,7 +6759,7 @@
// Perform updates.
r.overrideConfig = overrideConfig;
- r.mActivityWindowInfo = activityWindowInfo;
+ r.mActivityWindowInfo.set(activityWindowInfo);
final ViewRootImpl viewRoot = r.activity.mDecor != null
? r.activity.mDecor.getViewRootImpl() : null;
@@ -6792,11 +6792,10 @@
if (!activityWindowInfoFlag()) {
return;
}
- if (r.mActivityWindowInfo == null
- || r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) {
+ if (r.mActivityWindowInfo.equals(r.mLastReportedActivityWindowInfo)) {
return;
}
- r.mLastReportedActivityWindowInfo = r.mActivityWindowInfo;
+ r.mLastReportedActivityWindowInfo.set(r.mActivityWindowInfo);
ClientTransactionListenerController.getInstance().onActivityWindowInfoChanged(r.token,
r.mActivityWindowInfo);
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 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/app/servertransaction/ClientTransactionListenerController.java b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
index 7383d07..c55b0f1 100644
--- a/core/java/android/app/servertransaction/ClientTransactionListenerController.java
+++ b/core/java/android/app/servertransaction/ClientTransactionListenerController.java
@@ -122,7 +122,7 @@
}
for (Object activityWindowInfoChangedListener : activityWindowInfoChangedListeners) {
((BiConsumer<IBinder, ActivityWindowInfo>) activityWindowInfoChangedListener)
- .accept(activityToken, activityWindowInfo);
+ .accept(activityToken, new ActivityWindowInfo(activityWindowInfo));
}
}
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 5e00b7a..2c26389 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.registerDevicePresenceListenerService(deviceAddress,
+ mService.legacyStartObservingDevicePresence(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.unregisterDevicePresenceListenerService(deviceAddress,
+ mService.legacyStopObservingDevicePresence(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.notifyDeviceAppeared(associationId);
+ mService.notifySelfManagedDeviceAppeared(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.notifyDeviceDisappeared(associationId);
+ mService.notifySelfManagedDeviceDisappeared(associationId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index 57d59e5..1b00f90e 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -59,12 +59,16 @@
int userId);
@EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
- void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage,
- int userId);
+ void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId);
@EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
- void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage,
- int userId);
+ void legacyStopObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId);
+
+ @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+ void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
+
+ @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+ void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId);
@@ -93,9 +97,11 @@
@EnforcePermission("USE_COMPANION_TRANSPORTS")
void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener);
- void notifyDeviceAppeared(int associationId);
+ @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED")
+ void notifySelfManagedDeviceAppeared(int associationId);
- void notifyDeviceDisappeared(int associationId);
+ @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED")
+ void notifySelfManagedDeviceDisappeared(int associationId);
PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId,
int associationId);
@@ -135,10 +141,4 @@
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/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index bb0498e..229d119 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -9,14 +9,6 @@
flag {
namespace: "haptics"
- name: "haptics_customization_enabled"
- is_exported: true
- description: "Enables the haptics customization feature"
- bug: "241918098"
-}
-
-flag {
- namespace: "haptics"
name: "haptic_feedback_vibration_oem_customization_enabled"
description: "Enables OEMs/devices to customize vibrations for haptic feedback"
# Make read only. This is because the flag is used only once, and this could happen before
diff --git a/core/java/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/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/text/format/DateIntervalFormatTest.java b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
index de7244d..71c068d 100644
--- a/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
+++ b/core/tests/coretests/src/android/text/format/DateIntervalFormatTest.java
@@ -149,7 +149,7 @@
FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
assertEquals("19.–22.01.2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY,
FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
- assertEquals("19.01. – 22.04.2009",
+ assertEquals("19.01.\u2009–\u200922.04.2009",
formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * MONTH,
FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE));
assertEquals("19.01.2009\u2009\u2013\u200909.02.2012",
@@ -220,10 +220,10 @@
formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, 0));
assertEquals("19.–22. Jan. 2009", formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY,
FORMAT_SHOW_DATE | FORMAT_ABBREV_ALL));
- assertEquals("Mo., 19. – Do., 22. Jan. 2009",
+ assertEquals("Mo., 19.\u2009–\u2009Do., 22. Jan. 2009",
formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY,
FORMAT_SHOW_WEEKDAY | FORMAT_ABBREV_ALL));
- assertEquals("Montag, 19. – Donnerstag, 22. Januar 2009",
+ assertEquals("Montag, 19.\u2009–\u2009Donnerstag, 22. Januar 2009",
formatDateRange(de_DE, tz, fixedTime, fixedTime + 3 * DAY, FORMAT_SHOW_WEEKDAY));
assertEquals("19. Januar\u2009\u2013\u200922. April 2009",
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index 488f017..ff28055 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -1258,7 +1258,8 @@
@Test
public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() {
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
- android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+ android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
markWorkProfileUserAvailable();
setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE);
Intent sendIntent = createSendImageIntent();
@@ -1281,7 +1282,8 @@
@Test
public void testTriggerFromWorkProfile_inSingleUserMode() {
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
- android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE,
+ android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
markWorkProfileUserAvailable();
setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle);
Intent sendIntent = createSendImageIntent();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 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..cda9444 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -78,13 +78,13 @@
include_dirs: [
"external/skia/include/private",
"external/skia/src/core",
+ "external/skia/src/utils",
],
target: {
android: {
include_dirs: [
"external/skia/src/image",
- "external/skia/src/utils",
"external/skia/src/gpu",
"external/skia/src/shaders",
],
@@ -345,6 +345,7 @@
"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",
@@ -529,7 +529,9 @@
"effects/GainmapRenderer.cpp",
"pipeline/skia/BackdropFilterDrawable.cpp",
"pipeline/skia/HolePunch.cpp",
+ "pipeline/skia/SkiaCpuPipeline.cpp",
"pipeline/skia/SkiaDisplayList.cpp",
+ "pipeline/skia/SkiaPipeline.cpp",
"pipeline/skia/SkiaRecordingCanvas.cpp",
"pipeline/skia/StretchMask.cpp",
"pipeline/skia/RenderNodeDrawable.cpp",
@@ -539,6 +541,7 @@
"renderthread/RenderTask.cpp",
"renderthread/TimeLord.cpp",
"hwui/AnimatedImageDrawable.cpp",
+ "hwui/AnimatedImageThread.cpp",
"hwui/Bitmap.cpp",
"hwui/BlurDrawLooper.cpp",
"hwui/Canvas.cpp",
@@ -567,6 +570,7 @@
"HWUIProperties.sysprop",
"Interpolator.cpp",
"JankTracker.cpp",
+ "LayerUpdateQueue.cpp",
"LightingInfo.cpp",
"Matrix.cpp",
"Mesh.cpp",
@@ -599,14 +603,13 @@
local_include_dirs: ["platform/android"],
srcs: [
- "hwui/AnimatedImageThread.cpp",
"pipeline/skia/ATraceMemoryDump.cpp",
"pipeline/skia/GLFunctorDrawable.cpp",
"pipeline/skia/LayerDrawable.cpp",
"pipeline/skia/ShaderCache.cpp",
+ "pipeline/skia/SkiaGpuPipeline.cpp",
"pipeline/skia/SkiaMemoryTracer.cpp",
"pipeline/skia/SkiaOpenGLPipeline.cpp",
- "pipeline/skia/SkiaPipeline.cpp",
"pipeline/skia/SkiaProfileRenderer.cpp",
"pipeline/skia/SkiaVulkanPipeline.cpp",
"pipeline/skia/VkFunctorDrawable.cpp",
@@ -630,7 +633,6 @@
"DeferredLayerUpdater.cpp",
"HardwareBitmapUploader.cpp",
"Layer.cpp",
- "LayerUpdateQueue.cpp",
"ProfileDataContainer.cpp",
"Readback.cpp",
"TreeInfo.cpp",
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index ec53070..c1510d9 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -242,7 +242,7 @@
enum class OverdrawColorSet { Default = 0, Deuteranomaly };
-enum class RenderPipelineType { SkiaGL, SkiaVulkan, NotInitialized = 128 };
+enum class RenderPipelineType { SkiaGL, SkiaVulkan, SkiaCpu, NotInitialized = 128 };
enum class StretchEffectBehavior {
ShaderHWUI, // Stretch shader in HWUI only, matrix scale in SF
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp
index 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/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp
new file mode 100644
index 0000000..9b8373c
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pipeline/skia/SkiaCpuPipeline.h"
+
+#include <system/window.h>
+
+#include "DeviceInfo.h"
+#include "LightingInfo.h"
+#include "renderthread/Frame.h"
+#include "utils/Color.h"
+
+using namespace android::uirenderer::renderthread;
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+void SkiaCpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
+ // Render all layers that need to be updated, in order.
+ for (size_t i = 0; i < layers.entries().size(); i++) {
+ renderLayerImpl(layers.entries()[i].renderNode.get(), layers.entries()[i].damage);
+ }
+}
+
+// If the given node didn't have a layer surface, or had one of the wrong size, this method
+// creates a new one and returns true. Otherwise does nothing and returns false.
+bool SkiaCpuPipeline::createOrUpdateLayer(RenderNode* node,
+ const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) {
+ // compute the size of the surface (i.e. texture) to be allocated for this layer
+ const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
+ const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
+
+ SkSurface* layer = node->getLayerSurface();
+ if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
+ SkImageInfo info;
+ info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
+ kPremul_SkAlphaType, getSurfaceColorSpace());
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+ node->setLayerSurface(SkSurfaces::Raster(info, &props));
+ if (node->getLayerSurface()) {
+ // update the transform in window of the layer to reset its origin wrt light source
+ // position
+ Matrix4 windowTransform;
+ damageAccumulator.computeCurrentTransform(&windowTransform);
+ node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
+ } else {
+ String8 cachesOutput;
+ mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
+ &mRenderThread.renderState());
+ ALOGE("%s", cachesOutput.c_str());
+ if (errorHandler) {
+ std::ostringstream err;
+ err << "Unable to create layer for " << node->getName();
+ const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
+ err << ", size " << info.width() << "x" << info.height() << " max size "
+ << maxTextureSize << " color type " << (int)info.colorType() << " has context "
+ << (int)(mRenderThread.getGrContext() != nullptr);
+ errorHandler->onError(err.str());
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+MakeCurrentResult SkiaCpuPipeline::makeCurrent() {
+ return MakeCurrentResult::AlreadyCurrent;
+}
+
+Frame SkiaCpuPipeline::getFrame() {
+ return Frame(mSurface->width(), mSurface->height(), 0);
+}
+
+IRenderPipeline::DrawResult SkiaCpuPipeline::draw(
+ const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) {
+ LightingInfo::updateLighting(lightGeometry, lightInfo);
+ renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mSurface,
+ SkMatrix::I());
+ return {true, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd{}};
+}
+
+bool SkiaCpuPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) {
+ if (surface) {
+ ANativeWindowBuffer* buffer;
+ surface->dequeueBuffer(surface, &buffer, nullptr);
+ int width, height;
+ surface->query(surface, NATIVE_WINDOW_WIDTH, &width);
+ surface->query(surface, NATIVE_WINDOW_HEIGHT, &height);
+ SkImageInfo imageInfo =
+ SkImageInfo::Make(width, height, mSurfaceColorType,
+ SkAlphaType::kPremul_SkAlphaType, mSurfaceColorSpace);
+ size_t widthBytes = width * imageInfo.bytesPerPixel();
+ void* pixels = buffer->reserved[0];
+ mSurface = SkSurfaces::WrapPixels(imageInfo, pixels, widthBytes);
+ } else {
+ mSurface = sk_sp<SkSurface>();
+ }
+ return true;
+}
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.h b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h
new file mode 100644
index 0000000..5a1014c
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaCpuPipeline : public SkiaPipeline {
+public:
+ SkiaCpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {}
+ ~SkiaCpuPipeline() {}
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; }
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override {}
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override;
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override;
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {}
+ bool hasHardwareBuffer() override { return false; }
+
+ renderthread::MakeCurrentResult makeCurrent() override;
+ renderthread::Frame getFrame() override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams,
+ std::mutex& profilerLock) override;
+ bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult,
+ const SkRect& screenDirty, FrameInfo* currentFrameInfo,
+ bool* requireSwap) override {
+ return false;
+ }
+ DeferredLayerUpdater* createTextureLayer() override { return nullptr; }
+ bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
+ [[nodiscard]] android::base::unique_fd flush() override {
+ return android::base::unique_fd(-1);
+ };
+ void onStop() override {}
+ bool isSurfaceReady() override { return mSurface.get() != nullptr; }
+ bool isContextReady() override { return true; }
+
+ const SkM44& getPixelSnapMatrix() const override {
+ static const SkM44 sSnapMatrix = SkM44();
+ return sSnapMatrix;
+ }
+
+private:
+ sk_sp<SkSurface> mSurface;
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp
new file mode 100644
index 0000000..cd9daf4
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+#include <SkImageAndroid.h>
+#include <gui/TraceUtils.h>
+#include <include/android/SkSurfaceAndroid.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
+
+using namespace android::uirenderer::renderthread;
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+SkiaGpuPipeline::SkiaGpuPipeline(RenderThread& thread) : SkiaPipeline(thread) {}
+
+SkiaGpuPipeline::~SkiaGpuPipeline() {
+ unpinImages();
+}
+
+void SkiaGpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
+ sk_sp<GrDirectContext> cachedContext;
+
+ // Render all layers that need to be updated, in order.
+ for (size_t i = 0; i < layers.entries().size(); i++) {
+ RenderNode* layerNode = layers.entries()[i].renderNode.get();
+ renderLayerImpl(layerNode, layers.entries()[i].damage);
+ // cache the current context so that we can defer flushing it until
+ // either all the layers have been rendered or the context changes
+ GrDirectContext* currentContext =
+ GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
+ if (cachedContext.get() != currentContext) {
+ if (cachedContext.get()) {
+ ATRACE_NAME("flush layers (context changed)");
+ cachedContext->flushAndSubmit();
+ }
+ cachedContext.reset(SkSafeRef(currentContext));
+ }
+ }
+ if (cachedContext.get()) {
+ ATRACE_NAME("flush layers");
+ cachedContext->flushAndSubmit();
+ }
+}
+
+// If the given node didn't have a layer surface, or had one of the wrong size, this method
+// creates a new one and returns true. Otherwise does nothing and returns false.
+bool SkiaGpuPipeline::createOrUpdateLayer(RenderNode* node,
+ const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) {
+ // compute the size of the surface (i.e. texture) to be allocated for this layer
+ const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
+ const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
+
+ SkSurface* layer = node->getLayerSurface();
+ if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
+ SkImageInfo info;
+ info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
+ kPremul_SkAlphaType, getSurfaceColorSpace());
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+ SkASSERT(mRenderThread.getGrContext() != nullptr);
+ node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(),
+ skgpu::Budgeted::kYes, info, 0,
+ this->getSurfaceOrigin(), &props));
+ if (node->getLayerSurface()) {
+ // update the transform in window of the layer to reset its origin wrt light source
+ // position
+ Matrix4 windowTransform;
+ damageAccumulator.computeCurrentTransform(&windowTransform);
+ node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
+ } else {
+ String8 cachesOutput;
+ mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
+ &mRenderThread.renderState());
+ ALOGE("%s", cachesOutput.c_str());
+ if (errorHandler) {
+ std::ostringstream err;
+ err << "Unable to create layer for " << node->getName();
+ const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
+ err << ", size " << info.width() << "x" << info.height() << " max size "
+ << maxTextureSize << " color type " << (int)info.colorType() << " has context "
+ << (int)(mRenderThread.getGrContext() != nullptr);
+ errorHandler->onError(err.str());
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool SkiaGpuPipeline::pinImages(std::vector<SkImage*>& mutableImages) {
+ if (!mRenderThread.getGrContext()) {
+ ALOGD("Trying to pin an image with an invalid GrContext");
+ return false;
+ }
+ for (SkImage* image : mutableImages) {
+ if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) {
+ mPinnedImages.emplace_back(sk_ref_sp(image));
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+void SkiaGpuPipeline::unpinImages() {
+ for (auto& image : mPinnedImages) {
+ skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get());
+ }
+ mPinnedImages.clear();
+}
+
+void SkiaGpuPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
+ GrDirectContext* context = thread.getGrContext();
+ if (context && !bitmap->isHardware()) {
+ ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height());
+ auto image = bitmap->makeImage();
+ if (image.get()) {
+ skgpu::ganesh::PinAsTexture(context, image.get());
+ skgpu::ganesh::UnpinTexture(context, image.get());
+ // A submit is necessary as there may not be a frame coming soon, so without a call
+ // to submit these texture uploads can just sit in the queue building up until
+ // we run out of RAM
+ context->flushAndSubmit();
+ }
+ }
+}
+
+sk_sp<SkSurface> SkiaGpuPipeline::getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams) {
+ auto bufferColorSpace = bufferParams.getColorSpace();
+ if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
+ !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
+ mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer(
+ mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
+ bufferColorSpace, nullptr, true);
+ mBufferColorSpace = bufferColorSpace;
+ }
+ return mBufferSurface;
+}
+
+void SkiaGpuPipeline::dumpResourceCacheUsage() const {
+ int resources;
+ size_t bytes;
+ mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes);
+ size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit();
+
+ SkString log("Resource Cache Usage:\n");
+ log.appendf("%8d items\n", resources);
+ log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes,
+ bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f)));
+
+ ALOGD("%s", log.c_str());
+}
+
+void SkiaGpuPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+}
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index c8d5987..e4b1f91 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -14,25 +14,25 @@
* limitations under the License.
*/
-#include "SkiaOpenGLPipeline.h"
+#include "pipeline/skia/SkiaOpenGLPipeline.h"
-#include <include/gpu/ganesh/SkSurfaceGanesh.h>
-#include <include/gpu/ganesh/gl/GrGLBackendSurface.h>
-#include <include/gpu/gl/GrGLTypes.h>
#include <GrBackendSurface.h>
#include <SkBlendMode.h>
#include <SkImageInfo.h>
#include <cutils/properties.h>
#include <gui/TraceUtils.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
+#include <include/gpu/ganesh/gl/GrGLBackendSurface.h>
+#include <include/gpu/gl/GrGLTypes.h>
#include <strings.h>
#include "DeferredLayerUpdater.h"
#include "FrameInfo.h"
-#include "LayerDrawable.h"
#include "LightingInfo.h"
-#include "SkiaPipeline.h"
-#include "SkiaProfileRenderer.h"
#include "hwui/Bitmap.h"
+#include "pipeline/skia/LayerDrawable.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
+#include "pipeline/skia/SkiaProfileRenderer.h"
#include "private/hwui/DrawGlInfo.h"
#include "renderstate/RenderState.h"
#include "renderthread/EglManager.h"
@@ -47,7 +47,7 @@
namespace skiapipeline {
SkiaOpenGLPipeline::SkiaOpenGLPipeline(RenderThread& thread)
- : SkiaPipeline(thread), mEglManager(thread.eglManager()) {
+ : SkiaGpuPipeline(thread), mEglManager(thread.eglManager()) {
thread.renderState().registerContextCallback(this);
}
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 99469d1..2cfdd3f 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -14,11 +14,8 @@
* limitations under the License.
*/
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaPipeline.h"
-#include <include/android/SkSurfaceAndroid.h>
-#include <include/gpu/ganesh/SkSurfaceGanesh.h>
-#include <include/encode/SkPngEncoder.h>
#include <SkCanvas.h>
#include <SkColor.h>
#include <SkColorSpace.h>
@@ -40,6 +37,9 @@
#include <SkTypeface.h>
#include <android-base/properties.h>
#include <gui/TraceUtils.h>
+#include <include/android/SkSurfaceAndroid.h>
+#include <include/encode/SkPngEncoder.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
#include <unistd.h>
#include <sstream>
@@ -62,37 +62,13 @@
setSurfaceColorProperties(mColorMode);
}
-SkiaPipeline::~SkiaPipeline() {
- unpinImages();
-}
+SkiaPipeline::~SkiaPipeline() {}
void SkiaPipeline::onDestroyHardwareResources() {
unpinImages();
mRenderThread.cacheManager().trimStaleResources();
}
-bool SkiaPipeline::pinImages(std::vector<SkImage*>& mutableImages) {
- if (!mRenderThread.getGrContext()) {
- ALOGD("Trying to pin an image with an invalid GrContext");
- return false;
- }
- for (SkImage* image : mutableImages) {
- if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) {
- mPinnedImages.emplace_back(sk_ref_sp(image));
- } else {
- return false;
- }
- }
- return true;
-}
-
-void SkiaPipeline::unpinImages() {
- for (auto& image : mPinnedImages) {
- skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get());
- }
- mPinnedImages.clear();
-}
-
void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry,
LayerUpdateQueue* layerUpdateQueue, bool opaque,
const LightInfo& lightInfo) {
@@ -102,136 +78,53 @@
layerUpdateQueue->clear();
}
-void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
- sk_sp<GrDirectContext> cachedContext;
-
- // Render all layers that need to be updated, in order.
- for (size_t i = 0; i < layers.entries().size(); i++) {
- RenderNode* layerNode = layers.entries()[i].renderNode.get();
- // only schedule repaint if node still on layer - possible it may have been
- // removed during a dropped frame, but layers may still remain scheduled so
- // as not to lose info on what portion is damaged
- if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) {
- continue;
- }
- SkASSERT(layerNode->getLayerSurface());
- SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl();
- if (!displayList || displayList->isEmpty()) {
- ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
- return;
- }
-
- const Rect& layerDamage = layers.entries()[i].damage;
-
- SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
-
- int saveCount = layerCanvas->save();
- SkASSERT(saveCount == 1);
-
- layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());
-
- // TODO: put localized light center calculation and storage to a drawable related code.
- // It does not seem right to store something localized in a global state
- // fix here and in recordLayers
- const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
- Vector3 transformedLightCenter(savedLightCenter);
- // map current light center into RenderNode's coordinate space
- layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
- LightingInfo::setLightCenterRaw(transformedLightCenter);
-
- const RenderProperties& properties = layerNode->properties();
- const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
- if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) {
- return;
- }
-
- ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(),
- bounds.height());
-
- layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false;
- layerCanvas->clear(SK_ColorTRANSPARENT);
-
- RenderNodeDrawable root(layerNode, layerCanvas, false);
- root.forceDraw(layerCanvas);
- layerCanvas->restoreToCount(saveCount);
-
- LightingInfo::setLightCenterRaw(savedLightCenter);
-
- // cache the current context so that we can defer flushing it until
- // either all the layers have been rendered or the context changes
- GrDirectContext* currentContext =
- GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
- if (cachedContext.get() != currentContext) {
- if (cachedContext.get()) {
- ATRACE_NAME("flush layers (context changed)");
- cachedContext->flushAndSubmit();
- }
- cachedContext.reset(SkSafeRef(currentContext));
- }
+void SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) {
+ // only schedule repaint if node still on layer - possible it may have been
+ // removed during a dropped frame, but layers may still remain scheduled so
+ // as not to lose info on what portion is damaged
+ if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) {
+ return;
+ }
+ SkASSERT(layerNode->getLayerSurface());
+ SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl();
+ if (!displayList || displayList->isEmpty()) {
+ ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
+ return;
}
- if (cachedContext.get()) {
- ATRACE_NAME("flush layers");
- cachedContext->flushAndSubmit();
- }
-}
+ SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
-bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
- ErrorHandler* errorHandler) {
- // compute the size of the surface (i.e. texture) to be allocated for this layer
- const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
- const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
+ int saveCount = layerCanvas->save();
+ SkASSERT(saveCount == 1);
- SkSurface* layer = node->getLayerSurface();
- if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
- SkImageInfo info;
- info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
- kPremul_SkAlphaType, getSurfaceColorSpace());
- SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
- SkASSERT(mRenderThread.getGrContext() != nullptr);
- node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(),
- skgpu::Budgeted::kYes, info, 0,
- this->getSurfaceOrigin(), &props));
- if (node->getLayerSurface()) {
- // update the transform in window of the layer to reset its origin wrt light source
- // position
- Matrix4 windowTransform;
- damageAccumulator.computeCurrentTransform(&windowTransform);
- node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
- } else {
- String8 cachesOutput;
- mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
- &mRenderThread.renderState());
- ALOGE("%s", cachesOutput.c_str());
- if (errorHandler) {
- std::ostringstream err;
- err << "Unable to create layer for " << node->getName();
- const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
- err << ", size " << info.width() << "x" << info.height() << " max size "
- << maxTextureSize << " color type " << (int)info.colorType() << " has context "
- << (int)(mRenderThread.getGrContext() != nullptr);
- errorHandler->onError(err.str());
- }
- }
- return true;
- }
- return false;
-}
+ layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());
-void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
- GrDirectContext* context = thread.getGrContext();
- if (context && !bitmap->isHardware()) {
- ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height());
- auto image = bitmap->makeImage();
- if (image.get()) {
- skgpu::ganesh::PinAsTexture(context, image.get());
- skgpu::ganesh::UnpinTexture(context, image.get());
- // A submit is necessary as there may not be a frame coming soon, so without a call
- // to submit these texture uploads can just sit in the queue building up until
- // we run out of RAM
- context->flushAndSubmit();
- }
+ // TODO: put localized light center calculation and storage to a drawable related code.
+ // It does not seem right to store something localized in a global state
+ // fix here and in recordLayers
+ const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
+ Vector3 transformedLightCenter(savedLightCenter);
+ // map current light center into RenderNode's coordinate space
+ layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
+ LightingInfo::setLightCenterRaw(transformedLightCenter);
+
+ const RenderProperties& properties = layerNode->properties();
+ const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
+ if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) {
+ return;
}
+
+ ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(),
+ bounds.height());
+
+ layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false;
+ layerCanvas->clear(SK_ColorTRANSPARENT);
+
+ RenderNodeDrawable root(layerNode, layerCanvas, false);
+ root.forceDraw(layerCanvas);
+ layerCanvas->restoreToCount(saveCount);
+
+ LightingInfo::setLightCenterRaw(savedLightCenter);
}
static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) {
@@ -599,45 +492,6 @@
}
}
-void SkiaPipeline::dumpResourceCacheUsage() const {
- int resources;
- size_t bytes;
- mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes);
- size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit();
-
- SkString log("Resource Cache Usage:\n");
- log.appendf("%8d items\n", resources);
- log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes,
- bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f)));
-
- ALOGD("%s", log.c_str());
-}
-
-void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
- if (mHardwareBuffer) {
- AHardwareBuffer_release(mHardwareBuffer);
- mHardwareBuffer = nullptr;
- }
-
- if (buffer) {
- AHardwareBuffer_acquire(buffer);
- mHardwareBuffer = buffer;
- }
-}
-
-sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface(
- const renderthread::HardwareBufferRenderParams& bufferParams) {
- auto bufferColorSpace = bufferParams.getColorSpace();
- if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
- !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
- mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer(
- mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
- bufferColorSpace, nullptr, true);
- mBufferColorSpace = bufferColorSpace;
- }
- return mBufferSurface;
-}
-
void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mColorMode = colorMode;
switch (colorMode) {
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index befee89..f9d37b9 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -42,18 +42,9 @@
void onDestroyHardwareResources() override;
- bool pinImages(std::vector<SkImage*>& mutableImages) override;
- bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
- void unpinImages() override;
-
void renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
bool opaque, const LightInfo& lightInfo) override;
- // If the given node didn't have a layer surface, or had one of the wrong size, this method
- // creates a new one and returns true. Otherwise does nothing and returns false.
- bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
- ErrorHandler* errorHandler) override;
-
void setSurfaceColorProperties(ColorMode colorMode) override;
SkColorType getSurfaceColorType() const override { return mSurfaceColorType; }
sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; }
@@ -63,9 +54,8 @@
const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
const SkMatrix& preTransform);
- static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap);
-
- void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque);
+ void renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage);
+ virtual void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) = 0;
// Sets the recording callback to the provided function and the recording mode
// to CallbackAPI
@@ -75,19 +65,11 @@
mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None;
}
- virtual void setHardwareBuffer(AHardwareBuffer* buffer) override;
- bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
-
void setTargetSdrHdrRatio(float ratio) override;
protected:
- sk_sp<SkSurface> getBufferSkSurface(
- const renderthread::HardwareBufferRenderParams& bufferParams);
- void dumpResourceCacheUsage() const;
-
renderthread::RenderThread& mRenderThread;
- AHardwareBuffer* mHardwareBuffer = nullptr;
sk_sp<SkSurface> mBufferSurface = nullptr;
sk_sp<SkColorSpace> mBufferColorSpace = nullptr;
@@ -125,8 +107,6 @@
// Set up a multi frame capture.
bool setupMultiFrameCapture();
- std::vector<sk_sp<SkImage>> mPinnedImages;
-
// Block of properties used only for debugging to record a SkPicture and save it in a file.
// There are three possible ways of recording drawing commands.
enum class CaptureMode {
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index fd0a8e0..d06dba0 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "SkiaVulkanPipeline.h"
+#include "pipeline/skia/SkiaVulkanPipeline.h"
#include <GrDirectContext.h>
#include <GrTypes.h>
@@ -28,10 +28,10 @@
#include "DeferredLayerUpdater.h"
#include "LightingInfo.h"
#include "Readback.h"
-#include "ShaderCache.h"
-#include "SkiaPipeline.h"
-#include "SkiaProfileRenderer.h"
-#include "VkInteropFunctorDrawable.h"
+#include "pipeline/skia/ShaderCache.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
+#include "pipeline/skia/SkiaProfileRenderer.h"
+#include "pipeline/skia/VkInteropFunctorDrawable.h"
#include "renderstate/RenderState.h"
#include "renderthread/Frame.h"
#include "renderthread/IRenderPipeline.h"
@@ -42,7 +42,8 @@
namespace uirenderer {
namespace skiapipeline {
-SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {
+SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread)
+ : SkiaGpuPipeline(thread) {
thread.renderState().registerContextCallback(this);
}
diff --git a/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h
new file mode 100644
index 0000000..9159eae
--- /dev/null
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaGpuPipeline : public SkiaPipeline {
+public:
+ SkiaGpuPipeline(renderthread::RenderThread& thread);
+ virtual ~SkiaGpuPipeline();
+
+ virtual GrSurfaceOrigin getSurfaceOrigin() = 0;
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override;
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override;
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override;
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override;
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override;
+ bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
+
+ static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap);
+
+protected:
+ sk_sp<SkSurface> getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams);
+ void dumpResourceCacheUsage() const;
+
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+
+private:
+ std::vector<sk_sp<SkImage>> mPinnedImages;
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h
similarity index 95%
rename from libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
rename to libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h
index ebe8b6e..6e74782 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h
@@ -19,7 +19,7 @@
#include <EGL/egl.h>
#include <system/window.h>
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "renderstate/RenderState.h"
#include "renderthread/HardwareBufferRenderParams.h"
@@ -30,7 +30,7 @@
namespace uirenderer {
namespace skiapipeline {
-class SkiaOpenGLPipeline : public SkiaPipeline, public IGpuContextCallback {
+class SkiaOpenGLPipeline : public SkiaGpuPipeline, public IGpuContextCallback {
public:
SkiaOpenGLPipeline(renderthread::RenderThread& thread);
virtual ~SkiaOpenGLPipeline();
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h
similarity index 95%
rename from libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
rename to libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h
index 624eaa5..0d30df4 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h
@@ -17,7 +17,7 @@
#pragma once
#include "SkRefCnt.h"
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "renderstate/RenderState.h"
#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/VulkanManager.h"
@@ -30,7 +30,7 @@
namespace uirenderer {
namespace skiapipeline {
-class SkiaVulkanPipeline : public SkiaPipeline, public IGpuContextCallback {
+class SkiaVulkanPipeline : public SkiaGpuPipeline, public IGpuContextCallback {
public:
explicit SkiaVulkanPipeline(renderthread::RenderThread& thread);
virtual ~SkiaVulkanPipeline();
diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h
new file mode 120000
index 0000000..4fb4784
--- /dev/null
+++ b/libs/hwui/platform/host/android/api-level.h
@@ -0,0 +1 @@
+../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h
new file mode 100644
index 0000000..a717265
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+#include "renderthread/Frame.h"
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaGpuPipeline : public SkiaPipeline {
+public:
+ SkiaGpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {}
+ ~SkiaGpuPipeline() {}
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; }
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override {}
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override {
+ return false;
+ }
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override {}
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {}
+ bool hasHardwareBuffer() override { return false; }
+
+ renderthread::MakeCurrentResult makeCurrent() override {
+ return renderthread::MakeCurrentResult::Failed;
+ }
+ renderthread::Frame getFrame() override { return renderthread::Frame(0, 0, 0); }
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams,
+ std::mutex& profilerLock) override {
+ return {false, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd(-1)};
+ }
+ bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult,
+ const SkRect& screenDirty, FrameInfo* currentFrameInfo,
+ bool* requireSwap) override {
+ return false;
+ }
+ DeferredLayerUpdater* createTextureLayer() override { return nullptr; }
+ bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override {
+ return false;
+ }
+ [[nodiscard]] android::base::unique_fd flush() override {
+ return android::base::unique_fd(-1);
+ };
+ void onStop() override {}
+ bool isSurfaceReady() override { return false; }
+ bool isContextReady() override { return false; }
+
+ const SkM44& getPixelSnapMatrix() const override {
+ static const SkM44 sSnapMatrix = SkM44();
+ return sSnapMatrix;
+ }
+ static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h
new file mode 100644
index 0000000..4fafbcc
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaOpenGLPipeline : public SkiaGpuPipeline {
+public:
+ SkiaOpenGLPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {}
+
+ static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h
new file mode 100644
index 0000000..d54caef
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaVulkanPipeline : public SkiaGpuPipeline {
+public:
+ SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {}
+
+ static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index abf64d0..984916c 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -35,8 +35,8 @@
#include "Properties.h"
#include "RenderThread.h"
#include "hwui/Canvas.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "pipeline/skia/SkiaOpenGLPipeline.h"
-#include "pipeline/skia/SkiaPipeline.h"
#include "pipeline/skia/SkiaVulkanPipeline.h"
#include "thread/CommonPool.h"
#include "utils/GLUtils.h"
@@ -108,7 +108,7 @@
}
void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
- skiapipeline::SkiaPipeline::prepareToDraw(thread, bitmap);
+ skiapipeline::SkiaGpuPipeline::prepareToDraw(thread, bitmap);
}
CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index b8c3a4d..ee1d1f8 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -30,8 +30,6 @@
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
-class GrDirectContext;
-
struct ANativeWindow;
namespace android {
@@ -94,7 +92,6 @@
virtual void setSurfaceColorProperties(ColorMode colorMode) = 0;
virtual SkColorType getSurfaceColorType() const = 0;
virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0;
- virtual GrSurfaceOrigin getSurfaceOrigin() = 0;
virtual void setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0;
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
index 9111e61..68c2244 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.android.compose.theme.LocalAndroidColorScheme
@@ -39,7 +40,7 @@
text = text,
color = LocalAndroidColorScheme.current.onSurface,
textAlign = TextAlign.Center,
- style = MaterialTheme.typography.headlineSmall,
+ style = MaterialTheme.typography.headlineSmall.copy(hyphens = Hyphens.Auto),
)
}
@@ -52,7 +53,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = LocalAndroidColorScheme.current.onSurfaceVariant,
- style = MaterialTheme.typography.bodyMedium,
+ style = MaterialTheme.typography.bodyMedium.copy(hyphens = Hyphens.Auto),
)
}
@@ -70,7 +71,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = LocalAndroidColorScheme.current.onSurfaceVariant,
- style = MaterialTheme.typography.bodySmall,
+ style = MaterialTheme.typography.bodySmall.copy(hyphens = Hyphens.Auto),
overflow = TextOverflow.Ellipsis,
maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
onTextLayout = onTextLayout,
@@ -86,7 +87,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = LocalAndroidColorScheme.current.onSurface,
- style = MaterialTheme.typography.titleLarge,
+ style = MaterialTheme.typography.titleLarge.copy(hyphens = Hyphens.Auto),
)
}
@@ -104,7 +105,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = LocalAndroidColorScheme.current.onSurface,
- style = MaterialTheme.typography.titleSmall,
+ style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto),
overflow = TextOverflow.Ellipsis,
maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
onTextLayout = onTextLayout,
@@ -120,7 +121,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = color,
- style = MaterialTheme.typography.titleSmall,
+ style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto),
)
}
@@ -133,7 +134,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = MaterialTheme.colorScheme.inverseOnSurface,
- style = MaterialTheme.typography.bodyMedium,
+ style = MaterialTheme.typography.bodyMedium.copy(hyphens = Hyphens.Auto),
)
}
@@ -146,7 +147,7 @@
modifier = modifier.wrapContentSize(),
text = text,
color = MaterialTheme.colorScheme.inversePrimary,
- style = MaterialTheme.typography.labelLarge,
+ style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto),
)
}
@@ -160,7 +161,7 @@
text = text,
textAlign = TextAlign.Center,
color = LocalAndroidColorScheme.current.onSurfaceVariant,
- style = MaterialTheme.typography.labelLarge,
+ style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto),
)
}
@@ -173,6 +174,6 @@
modifier = modifier.wrapContentSize(),
text = text,
textAlign = TextAlign.Center,
- style = MaterialTheme.typography.labelLarge,
+ style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto),
)
}
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 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/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/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
similarity index 91%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index c308a98..2a2b2f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -85,7 +85,7 @@
}
@Test
- fun testIsImportantForAccessibility_falseWhenNoNotifs() =
+ fun isImportantForAccessibility_falseWhenNoNotifs() =
testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
@@ -100,7 +100,7 @@
}
@Test
- fun testIsImportantForAccessibility_trueWhenNotifs() =
+ fun isImportantForAccessibility_trueWhenNotifs() =
testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
@@ -115,7 +115,7 @@
}
@Test
- fun testIsImportantForAccessibility_trueWhenNotKeyguard() =
+ fun isImportantForAccessibility_trueWhenNotKeyguard() =
testScope.runTest {
val important by collectLastValue(underTest.isImportantForAccessibility)
@@ -130,7 +130,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() =
+ fun shouldIncludeEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -143,7 +143,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_falseWhenNotifs() =
+ fun shouldIncludeEmptyShadeView_falseWhenNotifs() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -156,7 +156,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
+ fun shouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -171,7 +171,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ fun shouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -189,7 +189,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() =
+ fun shouldIncludeEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -204,7 +204,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() =
+ fun shouldIncludeEmptyShadeView_falseWhenKeyguard() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -219,7 +219,7 @@
}
@Test
- fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
+ fun shouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
@@ -236,7 +236,7 @@
}
@Test
- fun testAreNotificationsHiddenInShade_true() =
+ fun areNotificationsHiddenInShade_true() =
testScope.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
@@ -248,7 +248,7 @@
}
@Test
- fun testAreNotificationsHiddenInShade_false() =
+ fun areNotificationsHiddenInShade_false() =
testScope.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
@@ -260,7 +260,7 @@
}
@Test
- fun testHasFilteredOutSeenNotifications_true() =
+ fun hasFilteredOutSeenNotifications_true() =
testScope.runTest {
val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
@@ -271,7 +271,7 @@
}
@Test
- fun testHasFilteredOutSeenNotifications_false() =
+ fun hasFilteredOutSeenNotifications_false() =
testScope.runTest {
val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
@@ -282,7 +282,7 @@
}
@Test
- fun testShouldIncludeFooterView_trueWhenShade() =
+ fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -298,7 +298,7 @@
}
@Test
- fun testShouldIncludeFooterView_trueWhenLockedShade() =
+ fun shouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -314,7 +314,7 @@
}
@Test
- fun testShouldIncludeFooterView_falseWhenKeyguard() =
+ fun shouldIncludeFooterView_falseWhenKeyguard() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -329,7 +329,7 @@
}
@Test
- fun testShouldIncludeFooterView_falseWhenUserNotSetUp() =
+ fun shouldIncludeFooterView_falseWhenUserNotSetUp() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -347,7 +347,7 @@
}
@Test
- fun testShouldIncludeFooterView_falseWhenStartingToSleep() =
+ fun shouldIncludeFooterView_falseWhenStartingToSleep() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -365,7 +365,7 @@
}
@Test
- fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() =
+ fun shouldIncludeFooterView_falseWhenQsExpandedDefault() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -384,7 +384,7 @@
}
@Test
- fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
+ fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -405,7 +405,7 @@
}
@Test
- fun testShouldIncludeFooterView_falseWhenRemoteInputActive() =
+ fun shouldIncludeFooterView_falseWhenRemoteInputActive() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -423,7 +423,7 @@
}
@Test
- fun testShouldIncludeFooterView_animatesWhenShade() =
+ fun shouldIncludeFooterView_animatesWhenShade() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -439,7 +439,7 @@
}
@Test
- fun testShouldIncludeFooterView_notAnimatingOnKeyguard() =
+ fun shouldIncludeFooterView_notAnimatingOnKeyguard() =
testScope.runTest {
val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
@@ -455,7 +455,7 @@
}
@Test
- fun testShouldHideFooterView_trueWhenShadeIsClosed() =
+ fun shouldHideFooterView_trueWhenShadeIsClosed() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -469,7 +469,7 @@
}
@Test
- fun testShouldHideFooterView_falseWhenShadeIsOpen() =
+ fun shouldHideFooterView_falseWhenShadeIsOpen() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -484,7 +484,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testPinnedHeadsUpRows_filtersForPinnedItems() =
+ fun pinnedHeadsUpRows_filtersForPinnedItems() =
testScope.runTest {
val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
@@ -527,7 +527,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testHasPinnedHeadsUpRows_true() =
+ fun hasPinnedHeadsUpRows_true() =
testScope.runTest {
val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
@@ -542,7 +542,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testHasPinnedHeadsUpRows_false() =
+ fun hasPinnedHeadsUpRows_false() =
testScope.runTest {
val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow)
@@ -557,7 +557,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testTopHeadsUpRow_emptyList_null() =
+ fun topHeadsUpRow_emptyList_null() =
testScope.runTest {
val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow)
@@ -569,7 +569,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testHeadsUpAnimationsEnabled_true() =
+ fun headsUpAnimationsEnabled_true() =
testScope.runTest {
val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
@@ -582,7 +582,7 @@
@Test
@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
- fun testHeadsUpAnimationsEnabled_keyguardShowing_false() =
+ fun headsUpAnimationsEnabled_keyguardShowing_false() =
testScope.runTest {
val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 5256bb9..f969ee6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -290,10 +290,15 @@
testScope.runTest {
val alpha by collectLastValue(underTest.glanceableHubAlpha)
- // Start on dream
- showDream()
+ // Start on lockscreen, notifications should be unhidden.
+ showLockscreen()
assertThat(alpha).isEqualTo(1f)
+ // Transition to dream, notifications should be hidden so that transition
+ // from dream->hub doesn't cause notification flicker.
+ showDream()
+ assertThat(alpha).isEqualTo(0f)
+
// Start transitioning to glanceable hub
val progress = 0.6f
keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 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/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/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index 188e7ee..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,6 +57,9 @@
private val biometricSettingsRepository: BiometricSettingsRepository,
private val systemClock: SystemClock,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val deviceEntryFingerprintAuthInteractor: Lazy<DeviceEntryFingerprintAuthInteractor>,
+ private val keyguardInteractor: Lazy<KeyguardInteractor>,
+ keyguardTransitionInteractor: Lazy<KeyguardTransitionInteractor>,
@Application scope: CoroutineScope,
) {
var receivedDownTouch = false
@@ -63,13 +77,80 @@
} 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/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/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/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/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/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 18bb5119..5544f93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -173,7 +173,6 @@
footerView,
footerViewModel,
clearAllNotifications = {
- metricsLogger.action(MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES)
clearAllNotifications(
parentView,
// Hide the silent section header (if present) if there will be
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index ecf737a..4a096a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
import android.view.WindowInsets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.communalHub
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
@@ -150,9 +151,11 @@
}
}
- launch {
- viewModel.glanceableHubAlpha.collect {
- controller.setMaxAlphaForGlanceableHub(it)
+ if (communalHub()) {
+ launch {
+ viewModel.glanceableHubAlpha.collect {
+ controller.setMaxAlphaForGlanceableHub(it)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index d112edb..f767b99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
@@ -241,7 +242,22 @@
started = SharingStarted.Eagerly,
initialValue = false,
)
- .dumpWhileCollecting("isOnGlanceableHubWithoutShade")
+ .dumpValue("isOnGlanceableHubWithoutShade")
+
+ /** Are we on the dream without the shade/qs? */
+ private val isDreamingWithoutShade: Flow<Boolean> =
+ combine(
+ keyguardTransitionInteractor.isFinishedInState(DREAMING),
+ isAnyExpanded,
+ ) { isDreaming, isAnyExpanded ->
+ isDreaming && !isAnyExpanded
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ )
+ .dumpValue("isDreamingWithoutShade")
/**
* Fade in if the user swipes the shade back up, not if collapsed by going to AOD. This is
@@ -460,6 +476,7 @@
combineTransform(
isOnGlanceableHubWithoutShade,
isOnLockscreen,
+ isDreamingWithoutShade,
merge(
lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
@@ -467,9 +484,9 @@
// Manually emit on start because [notificationAlpha] only starts emitting
// when transitions start.
.onStart { emit(1f) }
- ) { isOnGlanceableHubWithoutShade, isOnLockscreen, alpha,
+ ) { isOnGlanceableHubWithoutShade, isOnLockscreen, isDreamingWithoutShade, alpha,
->
- if (isOnGlanceableHubWithoutShade && !isOnLockscreen) {
+ if ((isOnGlanceableHubWithoutShade || isDreamingWithoutShade) && !isOnLockscreen) {
// Notifications should not be visible on the glanceable hub.
// TODO(b/321075734): implement a way to actually set the notifications to
// gone while on the hub instead of just adjusting alpha
@@ -484,6 +501,7 @@
emit(1f)
}
}
+ .distinctUntilChanged()
.dumpWhileCollecting("glanceableHubAlpha")
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 97fc35a..8b7b348 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -51,6 +51,9 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
@@ -59,6 +62,7 @@
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.SystemClock;
import dagger.Lazy;
@@ -286,7 +290,9 @@
VibratorHelper vibrator,
SystemClock systemClock,
Lazy<SelectedUserInteractor> selectedUserInteractor,
- BiometricUnlockInteractor biometricUnlockInteractor
+ BiometricUnlockInteractor biometricUnlockInteractor,
+ JavaAdapter javaAdapter,
+ KeyguardTransitionInteractor keyguardTransitionInteractor
) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
@@ -317,10 +323,19 @@
mOrderUnlockAndWake = resources.getBoolean(
com.android.internal.R.bool.config_orderUnlockAndWake);
mSelectedUserInteractor = selectedUserInteractor;
-
+ javaAdapter.alwaysCollectFlow(
+ keyguardTransitionInteractor.getStartedKeyguardTransitionStep(),
+ this::consumeTransitionStepOnStartedKeyguardState);
dumpManager.registerDumpable(this);
}
+ @VisibleForTesting
+ protected void consumeTransitionStepOnStartedKeyguardState(TransitionStep transitionStep) {
+ if (transitionStep.getFrom() == KeyguardState.GONE) {
+ mBiometricUnlockInteractor.setBiometricUnlockState(MODE_NONE);
+ }
+ }
+
public void setKeyguardViewController(KeyguardViewController keyguardViewController) {
mKeyguardViewController = keyguardViewController;
}
@@ -773,7 +788,6 @@
for (BiometricUnlockEventsListener listener : mBiometricUnlockEventsListeners) {
listener.onResetMode();
}
- mBiometricUnlockInteractor.setBiometricUnlockState(MODE_NONE);
mNumConsecutiveFpFailures = 0;
mLastFpFailureUptimeMillis = 0;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 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/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/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/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/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 6f65eb4..50f81ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.google.common.truth.Truth.assertThat;
@@ -26,6 +27,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -51,6 +53,10 @@
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.keyguard.domain.interactor.BiometricUnlockInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationMediaManager;
@@ -58,6 +64,7 @@
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
@@ -158,7 +165,9 @@
mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
mSystemClock,
() -> mSelectedUserInteractor,
- mBiometricUnlockInteractor
+ mBiometricUnlockInteractor,
+ mock(JavaAdapter.class),
+ mock(KeyguardTransitionInteractor.class)
);
biometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
biometricUnlockController.addListener(mBiometricUnlockEventsListener);
@@ -462,6 +471,29 @@
}
@Test
+ public void biometricUnlockStateResetOnTransitionFromGone() {
+ mBiometricUnlockController.consumeTransitionStepOnStartedKeyguardState(
+ new TransitionStep(
+ KeyguardState.AOD,
+ KeyguardState.GONE,
+ .1f /* value */,
+ TransitionState.STARTED
+ )
+ );
+ verify(mBiometricUnlockInteractor, never()).setBiometricUnlockState(anyInt());
+
+ mBiometricUnlockController.consumeTransitionStepOnStartedKeyguardState(
+ new TransitionStep(
+ KeyguardState.GONE,
+ KeyguardState.AOD,
+ .1f /* value */,
+ TransitionState.STARTED
+ )
+ );
+ verify(mBiometricUnlockInteractor).setBiometricUnlockState(eq(MODE_NONE));
+ }
+
+ @Test
public void onFingerprintDetect_showBouncer() {
// WHEN fingerprint detect occurs
mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
index 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/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/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
deleted file mode 100644
index 0a41485..0000000
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion;
-
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.annotation.UserIdInt;
-import android.companion.AssociationInfo;
-import android.companion.CompanionDeviceService;
-import android.companion.DevicePresenceEvent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.hardware.power.Mode;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.os.PowerManagerInternal;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.infra.PerUser;
-import com.android.server.companion.association.AssociationStore;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
-import com.android.server.companion.presence.ObservableUuid;
-import com.android.server.companion.presence.ObservableUuidStore;
-import com.android.server.companion.utils.PackageUtils;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Manages communication with companion applications via
- * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
- * the services, maintaining the connection (the binding), and invoking callback methods such as
- * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
- * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
- * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
- * application process.
- *
- * <p>
- * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
- * utilized by {@link CompanionDeviceManagerService}):
- * <ul>
- * <li> {@link #bindCompanionApplication(int, String, boolean)}
- * <li> {@link #unbindCompanionApplication(int, String)}
- * <li> {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)}
- * <li> {@link #isCompanionApplicationBound(int, String)}
- * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
- * </ul>
- *
- * @see CompanionDeviceService
- * @see android.companion.ICompanionDeviceService
- * @see CompanionDeviceServiceConnector
- */
-@SuppressLint("LongLogTag")
-public class CompanionApplicationController {
- static final boolean DEBUG = false;
- private static final String TAG = "CDM_CompanionApplicationController";
-
- private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
-
- private final @NonNull Context mContext;
- private final @NonNull AssociationStore mAssociationStore;
- private final @NonNull ObservableUuidStore mObservableUuidStore;
- private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
- private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
-
- private final PowerManagerInternal mPowerManagerInternal;
-
- @GuardedBy("mBoundCompanionApplications")
- private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
- mBoundCompanionApplications;
- @GuardedBy("mScheduledForRebindingCompanionApplications")
- private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
-
- CompanionApplicationController(Context context, AssociationStore associationStore,
- ObservableUuidStore observableUuidStore,
- CompanionDevicePresenceMonitor companionDevicePresenceMonitor,
- PowerManagerInternal powerManagerInternal) {
- mContext = context;
- mAssociationStore = associationStore;
- mObservableUuidStore = observableUuidStore;
- mDevicePresenceMonitor = companionDevicePresenceMonitor;
- mPowerManagerInternal = powerManagerInternal;
- mCompanionServicesRegister = new CompanionServicesRegister();
- mBoundCompanionApplications = new AndroidPackageMap<>();
- mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
- }
-
- void onPackagesChanged(@UserIdInt int userId) {
- mCompanionServicesRegister.invalidate(userId);
- }
-
- /**
- * CDM binds to the companion app.
- */
- public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName,
- boolean isSelfManaged) {
- if (DEBUG) {
- Log.i(TAG, "bind() u" + userId + "/" + packageName
- + " isSelfManaged=" + isSelfManaged);
- }
-
- final List<ComponentName> companionServices =
- mCompanionServicesRegister.forPackage(userId, packageName);
- if (companionServices.isEmpty()) {
- Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
- + "eligible CompanionDeviceService not found.\n"
- + "A CompanionDeviceService should declare an intent-filter for "
- + "\"android.companion.CompanionDeviceService\" action and require "
- + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
- return;
- }
-
- final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>();
- synchronized (mBoundCompanionApplications) {
- if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
- if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound.");
- return;
- }
-
- for (int i = 0; i < companionServices.size(); i++) {
- boolean isPrimary = i == 0;
- serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId,
- companionServices.get(i), isSelfManaged, isPrimary));
- }
-
- mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors);
- }
-
- // Set listeners for both Primary and Secondary connectors.
- for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
- serviceConnector.setListener(this::onBinderDied);
- }
-
- // Now "bind" all the connectors: the primary one and the rest of them.
- for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
- serviceConnector.connect();
- }
- }
-
- /**
- * CDM unbinds the companion app.
- */
- public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) {
- if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName);
-
- final List<CompanionDeviceServiceConnector> serviceConnectors;
-
- synchronized (mBoundCompanionApplications) {
- serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName);
- }
-
- synchronized (mScheduledForRebindingCompanionApplications) {
- mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
- }
-
- if (serviceConnectors == null) {
- if (DEBUG) {
- Log.e(TAG, "unbindCompanionApplication(): "
- + "u" + userId + "/" + packageName + " is NOT bound");
- Log.d(TAG, "Stacktrace", new Throwable());
- }
- return;
- }
-
- for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
- serviceConnector.postUnbind();
- }
- }
-
- /**
- * @return whether the companion application is bound now.
- */
- public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
- synchronized (mBoundCompanionApplications) {
- return mBoundCompanionApplications.containsValueForPackage(userId, packageName);
- }
- }
-
- private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
- CompanionDeviceServiceConnector serviceConnector) {
- Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
-
- if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
- if (DEBUG) {
- Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
- + serviceConnector.getComponentName());
- }
- return;
- }
-
- if (serviceConnector.isPrimary()) {
- synchronized (mScheduledForRebindingCompanionApplications) {
- mScheduledForRebindingCompanionApplications.setValueForPackage(
- userId, packageName, true);
- }
- }
-
- // Rebinding in 10 seconds.
- Handler.getMain().postDelayed(() ->
- onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector),
- REBIND_TIMEOUT);
- }
-
- private boolean isRebindingCompanionApplicationScheduled(
- @UserIdInt int userId, @NonNull String packageName) {
- synchronized (mScheduledForRebindingCompanionApplications) {
- return mScheduledForRebindingCompanionApplications.containsValueForPackage(
- userId, packageName);
- }
- }
-
- private void onRebindingCompanionApplicationTimeout(
- @UserIdInt int userId, @NonNull String packageName,
- @NonNull CompanionDeviceServiceConnector serviceConnector) {
- // Re-mark the application is bound.
- if (serviceConnector.isPrimary()) {
- synchronized (mBoundCompanionApplications) {
- if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
- List<CompanionDeviceServiceConnector> serviceConnectors =
- Collections.singletonList(serviceConnector);
- mBoundCompanionApplications.setValueForPackage(userId, packageName,
- serviceConnectors);
- }
- }
-
- synchronized (mScheduledForRebindingCompanionApplications) {
- mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
- }
- }
-
- serviceConnector.connect();
- }
-
- /**
- * Notify the app that the device appeared.
- *
- * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead
- */
- @Deprecated
- public void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) {
- final int userId = association.getUserId();
- final String packageName = association.getPackageName();
-
- Slog.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId
- + "/" + packageName);
-
- final CompanionDeviceServiceConnector primaryServiceConnector =
- getPrimaryServiceConnector(userId, packageName);
- if (primaryServiceConnector == null) {
- Slog.e(TAG, "notify_CompanionApplicationDevice_Appeared(): "
- + "u" + userId + "/" + packageName + " is NOT bound.");
- Slog.e(TAG, "Stacktrace", new Throwable());
- return;
- }
-
- Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=["
- + packageName + "] associationId=[" + association.getId() + "]");
-
- primaryServiceConnector.postOnDeviceAppeared(association);
- }
-
- /**
- * Notify the app that the device disappeared.
- *
- * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead
- */
- @Deprecated
- public void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) {
- final int userId = association.getUserId();
- final String packageName = association.getPackageName();
-
- Slog.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId
- + "/" + packageName);
-
- final CompanionDeviceServiceConnector primaryServiceConnector =
- getPrimaryServiceConnector(userId, packageName);
- if (primaryServiceConnector == null) {
- Slog.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): "
- + "u" + userId + "/" + packageName + " is NOT bound.");
- Slog.e(TAG, "Stacktrace", new Throwable());
- return;
- }
-
- Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=["
- + packageName + "] associationId=[" + association.getId() + "]");
-
- primaryServiceConnector.postOnDeviceDisappeared(association);
- }
-
- /**
- * Notify the app that the device appeared.
- */
- public void notifyCompanionDevicePresenceEvent(AssociationInfo association, int event) {
- final int userId = association.getUserId();
- final String packageName = association.getPackageName();
- final CompanionDeviceServiceConnector primaryServiceConnector =
- getPrimaryServiceConnector(userId, packageName);
- final DevicePresenceEvent devicePresenceEvent =
- new DevicePresenceEvent(association.getId(), event, null);
-
- if (primaryServiceConnector == null) {
- Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): "
- + "u" + userId + "/" + packageName
- + " event=[ " + event + " ] is NOT bound.");
- Slog.e(TAG, "Stacktrace", new Throwable());
- return;
- }
-
- Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
- + packageName + "] associationId=[" + association.getId()
- + "] event=[" + event + "]");
-
- primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
- }
-
- /**
- * Notify the app that the device disappeared.
- */
- public void notifyUuidDevicePresenceEvent(ObservableUuid uuid, int event) {
- final int userId = uuid.getUserId();
- final ParcelUuid parcelUuid = uuid.getUuid();
- final String packageName = uuid.getPackageName();
- final CompanionDeviceServiceConnector primaryServiceConnector =
- getPrimaryServiceConnector(userId, packageName);
- final DevicePresenceEvent devicePresenceEvent =
- new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid);
-
- if (primaryServiceConnector == null) {
- Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): "
- + "u" + userId + "/" + packageName
- + " event=[ " + event + " ] is NOT bound.");
- Slog.e(TAG, "Stacktrace", new Throwable());
- return;
- }
-
- Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
- + packageName + "]" + "event= [" + event + "]");
-
- primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
- }
-
- void dump(@NonNull PrintWriter out) {
- out.append("Companion Device Application Controller: \n");
-
- synchronized (mBoundCompanionApplications) {
- out.append(" Bound Companion Applications: ");
- if (mBoundCompanionApplications.size() == 0) {
- out.append("<empty>\n");
- } else {
- out.append("\n");
- mBoundCompanionApplications.dump(out);
- }
- }
-
- out.append(" Companion Applications Scheduled For Rebinding: ");
- if (mScheduledForRebindingCompanionApplications.size() == 0) {
- out.append("<empty>\n");
- } else {
- out.append("\n");
- mScheduledForRebindingCompanionApplications.dump(out);
- }
- }
-
- /**
- * Rebinding for Self-Managed secondary services OR Non-Self-Managed services.
- */
- private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
- @NonNull CompanionDeviceServiceConnector serviceConnector) {
-
- boolean isPrimary = serviceConnector.isPrimary();
- Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
-
- // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY.
- if (isPrimary) {
- final List<AssociationInfo> associations =
- mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
-
- for (AssociationInfo association : associations) {
- final String deviceProfile = association.getDeviceProfile();
- if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
- Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
- mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
- break;
- }
- }
-
- synchronized (mBoundCompanionApplications) {
- mBoundCompanionApplications.removePackage(userId, packageName);
- }
- }
-
- // Second: schedule rebinding if needed.
- final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
-
- if (shouldScheduleRebind) {
- scheduleRebinding(userId, packageName, serviceConnector);
- }
- }
-
- private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector(
- @UserIdInt int userId, @NonNull String packageName) {
- final List<CompanionDeviceServiceConnector> connectors;
- synchronized (mBoundCompanionApplications) {
- connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName);
- }
- return connectors != null ? connectors.get(0) : null;
- }
-
- private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
- // Make sure do not schedule rebind for the case ServiceConnector still gets callback after
- // app is uninstalled.
- boolean stillAssociated = false;
- // Make sure to clean up the state for all the associations
- // that associate with this package.
- boolean shouldScheduleRebind = false;
- boolean shouldScheduleRebindForUuid = false;
- final List<ObservableUuid> uuids =
- mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
- for (AssociationInfo ai :
- mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) {
- final int associationId = ai.getId();
- stillAssociated = true;
- if (ai.isSelfManaged()) {
- // Do not rebind if primary one is died for selfManaged application.
- if (isPrimary
- && mDevicePresenceMonitor.isDevicePresent(associationId)) {
- mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId);
- shouldScheduleRebind = false;
- }
- // Do not rebind if both primary and secondary services are died for
- // selfManaged application.
- shouldScheduleRebind = isCompanionApplicationBound(userId, packageName);
- } else if (ai.isNotifyOnDeviceNearby()) {
- // Always rebind for non-selfManaged devices.
- shouldScheduleRebind = true;
- }
- }
-
- for (ObservableUuid uuid : uuids) {
- if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
- shouldScheduleRebindForUuid = true;
- break;
- }
- }
-
- return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
- }
-
- private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
- @Override
- public synchronized @NonNull Map<String, List<ComponentName>> forUser(
- @UserIdInt int userId) {
- return super.forUser(userId);
- }
-
- synchronized @NonNull List<ComponentName> forPackage(
- @UserIdInt int userId, @NonNull String packageName) {
- return forUser(userId).getOrDefault(packageName, Collections.emptyList());
- }
-
- synchronized void invalidate(@UserIdInt int userId) {
- remove(userId);
- }
-
- @Override
- protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
- return PackageUtils.getCompanionServicesForUser(mContext, userId);
- }
- }
-
- /**
- * Associates an Android package (defined by userId + packageName) with a value of type T.
- */
- private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> {
-
- void setValueForPackage(
- @UserIdInt int userId, @NonNull String packageName, @NonNull T value) {
- Map<String, T> forUser = get(userId);
- if (forUser == null) {
- forUser = /* Map<String, T> */ new HashMap();
- put(userId, forUser);
- }
-
- forUser.put(packageName, value);
- }
-
- boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
- final Map<String, ?> forUser = get(userId);
- return forUser != null && forUser.containsKey(packageName);
- }
-
- T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
- final Map<String, T> forUser = get(userId);
- return forUser != null ? forUser.get(packageName) : null;
- }
-
- T removePackage(@UserIdInt int userId, @NonNull String packageName) {
- final Map<String, T> forUser = get(userId);
- if (forUser == null) return null;
- return forUser.remove(packageName);
- }
-
- void dump() {
- if (size() == 0) {
- Log.d(TAG, "<empty>");
- return;
- }
-
- for (int i = 0; i < size(); i++) {
- final int userId = keyAt(i);
- final Map<String, T> forUser = get(userId);
- if (forUser.isEmpty()) {
- Log.d(TAG, "u" + userId + ": <empty>");
- }
-
- for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
- final String packageName = packageValue.getKey();
- final T value = packageValue.getValue();
- Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value);
- }
- }
- }
-
- private void dump(@NonNull PrintWriter out) {
- for (int i = 0; i < size(); i++) {
- final int userId = keyAt(i);
- final Map<String, T> forUser = get(userId);
- if (forUser.isEmpty()) {
- out.append(" u").append(String.valueOf(userId)).append(": <empty>\n");
- }
-
- for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
- final String packageName = packageValue.getKey();
- final T value = packageValue.getValue();
- out.append(" u").append(String.valueOf(userId)).append("\\")
- .append(packageName).append(" -> ")
- .append(value.toString()).append('\n');
- }
- }
- }
- }
-}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 712162b..edf9fd1 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -20,15 +20,10 @@
import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
+import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
@@ -42,13 +37,10 @@
import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
-import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
-import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks;
import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.MINUTES;
import android.annotation.EnforcePermission;
@@ -64,7 +56,6 @@
import android.bluetooth.BluetoothDevice;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
-import android.companion.DeviceNotAssociatedException;
import android.companion.IAssociationRequestCallback;
import android.companion.ICompanionDeviceManager;
import android.companion.IOnAssociationsChangedListener;
@@ -79,7 +70,6 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
-import android.hardware.power.Mode;
import android.net.MacAddress;
import android.net.NetworkPolicyManager;
import android.os.Binder;
@@ -91,7 +81,6 @@
import android.os.PowerManagerInternal;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
@@ -118,7 +107,8 @@
import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall;
import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.CompanionAppBinder;
+import com.android.server.companion.presence.DevicePresenceProcessor;
import com.android.server.companion.presence.ObservableUuid;
import com.android.server.companion.presence.ObservableUuidStore;
import com.android.server.companion.transport.CompanionTransportManager;
@@ -131,10 +121,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
@SuppressLint("LongLogTag")
@@ -146,10 +133,6 @@
private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
- private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
- "debug.cdm.cdmservice.removal_time_window";
-
- private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
private static final int MAX_CN_LENGTH = 500;
private final ActivityTaskManagerInternal mAtmInternal;
@@ -165,8 +148,8 @@
private final AssociationRequestsProcessor mAssociationRequestsProcessor;
private final SystemDataTransferProcessor mSystemDataTransferProcessor;
private final BackupRestoreProcessor mBackupRestoreProcessor;
- private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
- private final CompanionApplicationController mCompanionAppController;
+ private final DevicePresenceProcessor mDevicePresenceProcessor;
+ private final CompanionAppBinder mCompanionAppBinder;
private final CompanionTransportManager mTransportManager;
private final DisassociationProcessor mDisassociationProcessor;
private final CrossDeviceSyncController mCrossDeviceSyncController;
@@ -185,7 +168,7 @@
mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
final AssociationDiskStore associationDiskStore = new AssociationDiskStore();
- mAssociationStore = new AssociationStore(userManager, associationDiskStore);
+ mAssociationStore = new AssociationStore(context, userManager, associationDiskStore);
mSystemDataTransferRequestStore = new SystemDataTransferRequestStore();
mObservableUuidStore = new ObservableUuidStore();
@@ -196,18 +179,17 @@
mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
mAssociationRequestsProcessor);
- mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager,
- mAssociationStore, mObservableUuidStore, mDevicePresenceCallback);
+ mCompanionAppBinder = new CompanionAppBinder(context);
- mCompanionAppController = new CompanionApplicationController(
- context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor,
+ mDevicePresenceProcessor = new DevicePresenceProcessor(context,
+ mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
mPowerManagerInternal);
mTransportManager = new CompanionTransportManager(context, mAssociationStore);
mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
- mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor,
- mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager);
+ mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
+ mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
mPackageManagerInternal, mAssociationStore,
@@ -242,7 +224,7 @@
// delays (even in case of the Main Thread). It may be fine overall, but would require
// updating the tests (adding a delay there).
mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true);
- mDevicePresenceMonitor.init(context);
+ mDevicePresenceProcessor.init(context);
} else if (phase == PHASE_BOOT_COMPLETED) {
// Run the Inactive Association Removal job service daily.
InactiveAssociationsRemovalService.schedule(getContext());
@@ -271,7 +253,7 @@
// Notify and bind the app after the phone is unlocked.
final int userId = user.getUserIdentifier();
final Set<BluetoothDevice> blueToothDevices =
- mDevicePresenceMonitor.getPendingConnectedDevices().get(userId);
+ mDevicePresenceProcessor.getPendingConnectedDevices().get(userId);
final List<ObservableUuid> observableUuids =
mObservableUuidStore.getObservableUuidsForUser(userId);
@@ -287,14 +269,14 @@
mAssociationStore.getActiveAssociationsByAddress(
bluetoothDevice.getAddress())) {
Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
- mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId());
+ mDevicePresenceProcessor.onBluetoothCompanionDeviceConnected(ai.getId());
}
for (ObservableUuid observableUuid : observableUuids) {
if (deviceUuids.contains(observableUuid.getUuid())) {
Slog.i(TAG, "onUserUnlocked, UUID( "
+ observableUuid.getUuid() + " ) is connected");
- mDevicePresenceMonitor.onDevicePresenceEventByUuid(
+ mDevicePresenceProcessor.onDevicePresenceEventByUuid(
observableUuid, EVENT_BT_CONNECTED);
}
}
@@ -302,181 +284,6 @@
}
}
- @NonNull
- AssociationInfo getAssociationWithCallerChecks(
- @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
- AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(
- userId, packageName, macAddress);
- association = sanitizeWithCallerChecks(getContext(), association);
- if (association != null) {
- return association;
- } else {
- throw new IllegalArgumentException("Association does not exist "
- + "or the caller does not have permissions to manage it "
- + "(ie. it belongs to a different package or a different user).");
- }
- }
-
- @NonNull
- AssociationInfo getAssociationWithCallerChecks(int associationId) {
- AssociationInfo association = mAssociationStore.getAssociationById(associationId);
- association = sanitizeWithCallerChecks(getContext(), association);
- if (association != null) {
- return association;
- } else {
- throw new IllegalArgumentException("Association does not exist "
- + "or the caller does not have permissions to manage it "
- + "(ie. it belongs to a different package or a different user).");
- }
- }
-
- private void onDeviceAppearedInternal(int associationId) {
- if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId);
-
- final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
- if (DEBUG) Log.d(TAG, " association=" + association);
-
- if (!association.shouldBindWhenPresent()) return;
-
- bindApplicationIfNeeded(association);
-
- mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association);
- }
-
- private void onDeviceDisappearedInternal(int associationId) {
- if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId);
-
- final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
- if (DEBUG) Log.d(TAG, " association=" + association);
-
- final int userId = association.getUserId();
- final String packageName = association.getPackageName();
-
- if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
- if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
- return;
- }
-
- if (association.shouldBindWhenPresent()) {
- mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association);
- }
- }
-
- private void onDevicePresenceEventInternal(int associationId, int event) {
- Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event);
- final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
- final String packageName = association.getPackageName();
- final int userId = association.getUserId();
- switch (event) {
- case EVENT_BLE_APPEARED:
- case EVENT_BT_CONNECTED:
- case EVENT_SELF_MANAGED_APPEARED:
- if (!association.shouldBindWhenPresent()) return;
-
- bindApplicationIfNeeded(association);
-
- mCompanionAppController.notifyCompanionDevicePresenceEvent(
- association, event);
- break;
- case EVENT_BLE_DISAPPEARED:
- case EVENT_BT_DISCONNECTED:
- case EVENT_SELF_MANAGED_DISAPPEARED:
- if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
- if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
- return;
- }
- if (association.shouldBindWhenPresent()) {
- mCompanionAppController.notifyCompanionDevicePresenceEvent(
- association, event);
- }
- // Check if there are other devices associated to the app that are present.
- if (shouldBindPackage(userId, packageName)) return;
- mCompanionAppController.unbindCompanionApplication(userId, packageName);
- break;
- default:
- Slog.e(TAG, "Event: " + event + "is not supported");
- break;
- }
- }
-
- private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) {
- Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid()
- + "for package=" + uuid.getPackageName() + " event=" + event);
- final String packageName = uuid.getPackageName();
- final int userId = uuid.getUserId();
-
- switch (event) {
- case EVENT_BT_CONNECTED:
- if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
- mCompanionAppController.bindCompanionApplication(
- userId, packageName, /*bindImportant*/ false);
-
- } else if (DEBUG) {
- Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
- }
-
- mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event);
-
- break;
- case EVENT_BT_DISCONNECTED:
- if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
- if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
- return;
- }
-
- mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event);
- // Check if there are other devices associated to the app or the UUID to be
- // observed are present.
- if (shouldBindPackage(userId, packageName)) return;
-
- mCompanionAppController.unbindCompanionApplication(userId, packageName);
-
- break;
- default:
- Slog.e(TAG, "Event: " + event + "is not supported");
- break;
- }
- }
-
- private void bindApplicationIfNeeded(AssociationInfo association) {
- final String packageName = association.getPackageName();
- final int userId = association.getUserId();
- // Set bindImportant to true when the association is self-managed to avoid the target
- // service being killed.
- final boolean bindImportant = association.isSelfManaged();
- if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
- mCompanionAppController.bindCompanionApplication(
- userId, packageName, bindImportant);
- } else if (DEBUG) {
- Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
- }
- }
-
- /**
- * @return whether the package should be bound (i.e. at least one of the devices associated with
- * the package is currently present OR the UUID to be observed by this package is
- * currently present).
- */
- private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
- final List<AssociationInfo> packageAssociations =
- mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
- final List<ObservableUuid> observableUuids =
- mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
- for (AssociationInfo association : packageAssociations) {
- if (!association.shouldBindWhenPresent()) continue;
- if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true;
- }
-
- for (ObservableUuid uuid : observableUuids) {
- if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
- return true;
- }
- }
-
- return false;
- }
-
private void onPackageRemoveOrDataClearedInternal(
@UserIdInt int userId, @NonNull String packageName) {
if (DEBUG) {
@@ -502,7 +309,7 @@
mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
}
- mCompanionAppController.onPackagesChanged(userId);
+ mCompanionAppBinder.onPackagesChanged(userId);
}
private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -515,34 +322,15 @@
association.getPackageName());
}
- mCompanionAppController.onPackagesChanged(userId);
+ mCompanionAppBinder.onPackagesChanged(userId);
}
private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
mBackupRestoreProcessor.restorePendingAssociations(userId, packageName);
}
- // Revoke associations if the selfManaged companion device does not connect for 3 months.
void removeInactiveSelfManagedAssociations() {
- final long currentTime = System.currentTimeMillis();
- long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
- if (removalWindow <= 0) {
- // 0 or negative values indicate that the sysprop was never set or should be ignored.
- removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
- }
-
- for (AssociationInfo association : mAssociationStore.getAssociations()) {
- if (!association.isSelfManaged()) continue;
-
- final boolean isInactive =
- currentTime - association.getLastTimeConnectedMs() >= removalWindow;
- if (!isInactive) continue;
-
- final int id = association.getId();
-
- Slog.i(TAG, "Removing inactive self-managed association id=" + id);
- mDisassociationProcessor.disassociate(id);
- }
+ mDisassociationProcessor.removeIdleSelfManagedAssociations();
}
public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
@@ -679,24 +467,15 @@
@Deprecated
@Override
public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) {
- Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName
- + ", macAddress=" + deviceMacAddress);
-
requireNonNull(deviceMacAddress);
requireNonNull(packageName);
- final AssociationInfo association =
- getAssociationWithCallerChecks(userId, packageName, deviceMacAddress);
- mDisassociationProcessor.disassociate(association.getId());
+ mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress);
}
@Override
public void disassociate(int associationId) {
- Slog.i(TAG, "disassociate() associationId=" + associationId);
-
- final AssociationInfo association =
- getAssociationWithCallerChecks(associationId);
- mDisassociationProcessor.disassociate(association.getId());
+ mDisassociationProcessor.disassociate(associationId);
}
@Override
@@ -758,21 +537,25 @@
}
@Override
+ @Deprecated
@EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
- public void registerDevicePresenceListenerService(String deviceAddress,
- String callingPackage, int userId) throws RemoteException {
- registerDevicePresenceListenerService_enforcePermission();
- // TODO: take the userId into account.
- registerDevicePresenceListenerActive(callingPackage, deviceAddress, true);
+ public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage,
+ int userId) throws RemoteException {
+ legacyStartObservingDevicePresence_enforcePermission();
+
+ mDevicePresenceProcessor.startObservingDevicePresence(userId, callingPackage,
+ deviceAddress);
}
@Override
+ @Deprecated
@EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
- public void unregisterDevicePresenceListenerService(String deviceAddress,
- String callingPackage, int userId) throws RemoteException {
- unregisterDevicePresenceListenerService_enforcePermission();
- // TODO: take the userId into account.
- registerDevicePresenceListenerActive(callingPackage, deviceAddress, false);
+ public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage,
+ int userId) throws RemoteException {
+ legacyStopObservingDevicePresence_enforcePermission();
+
+ mDevicePresenceProcessor.stopObservingDevicePresence(userId, callingPackage,
+ deviceAddress);
}
@Override
@@ -780,7 +563,8 @@
public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
String packageName, int userId) {
startObservingDevicePresence_enforcePermission();
- registerDevicePresenceListener(request, packageName, userId, /* active */ true);
+
+ mDevicePresenceProcessor.startObservingDevicePresence(request, packageName, userId);
}
@Override
@@ -788,80 +572,8 @@
public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
String packageName, int userId) {
stopObservingDevicePresence_enforcePermission();
- registerDevicePresenceListener(request, packageName, userId, /* active */ false);
- }
- private void registerDevicePresenceListener(ObservingDevicePresenceRequest request,
- String packageName, int userId, boolean active) {
- enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
- enforceCallerIsSystemOr(userId, packageName);
-
- final int associationId = request.getAssociationId();
- final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
- associationId);
- final ParcelUuid uuid = request.getUuid();
-
- if (uuid != null) {
- enforceCallerCanObservingDevicePresenceByUuid(getContext());
- if (active) {
- startObservingDevicePresenceByUuid(uuid, packageName, userId);
- } else {
- stopObservingDevicePresenceByUuid(uuid, packageName, userId);
- }
- } else if (associationInfo == null) {
- throw new IllegalArgumentException("App " + packageName
- + " is not associated with device " + request.getAssociationId()
- + " for user " + userId);
- } else {
- processDevicePresenceListener(
- associationInfo, userId, packageName, active);
- }
- }
-
- private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
- int userId) {
- final List<ObservableUuid> observableUuids =
- mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
- for (ObservableUuid observableUuid : observableUuids) {
- if (observableUuid.getUuid().equals(uuid)) {
- Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName
- + "has been already scheduled for observing");
- return;
- }
- }
-
- final ObservableUuid observableUuid = new ObservableUuid(userId, uuid,
- packageName, System.currentTimeMillis());
-
- mObservableUuidStore.writeObservableUuid(userId, observableUuid);
- }
-
- private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
- int userId) {
- final List<ObservableUuid> uuidsTobeObserved =
- mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
- boolean isScheduledObserving = false;
-
- for (ObservableUuid observableUuid : uuidsTobeObserved) {
- if (observableUuid.getUuid().equals(uuid)) {
- isScheduledObserving = true;
- break;
- }
- }
-
- if (!isScheduledObserving) {
- Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName
- + "has NOT been scheduled for observing yet");
- return;
- }
-
- mObservableUuidStore.removeObservableUuid(userId, uuid, packageName);
- mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid);
-
- if (!shouldBindPackage(userId, packageName)) {
- mCompanionAppController.unbindCompanionApplication(userId, packageName);
- }
+ mDevicePresenceProcessor.stopObservingDevicePresence(request, packageName, userId);
}
@Override
@@ -874,8 +586,7 @@
@Override
public boolean isPermissionTransferUserConsented(String packageName, int userId,
int associationId) {
- return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName,
- userId, associationId);
+ return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId);
}
@Override
@@ -891,8 +602,7 @@
ParcelFileDescriptor fd) {
attachSystemDataTransport_enforcePermission();
- getAssociationWithCallerChecks(associationId);
- mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd);
+ mTransportManager.attachSystemDataTransport(associationId, fd);
}
@Override
@@ -900,161 +610,61 @@
public void detachSystemDataTransport(String packageName, int userId, int associationId) {
detachSystemDataTransport_enforcePermission();
- 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);
+ mTransportManager.detachSystemDataTransport(associationId);
}
@Override
@EnforcePermission(MANAGE_COMPANION_DEVICES)
public void enableSecureTransport(boolean enabled) {
enableSecureTransport_enforcePermission();
+
mTransportManager.enableSecureTransport(enabled);
}
@Override
- public void notifyDeviceAppeared(int associationId) {
- if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId);
-
- AssociationInfo association = getAssociationWithCallerChecks(associationId);
- if (!association.isSelfManaged()) {
- throw new IllegalArgumentException("Association with ID " + associationId
- + " is not self-managed. notifyDeviceAppeared(int) can only be called for"
- + " self-managed associations.");
- }
- // AssociationInfo class is immutable: create a new AssociationInfo object with updated
- // timestamp.
- association = (new AssociationInfo.Builder(association))
- .setLastTimeConnected(System.currentTimeMillis())
- .build();
- mAssociationStore.updateAssociation(association);
-
- mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
-
- final String deviceProfile = association.getDeviceProfile();
- if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
- Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile);
- mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true);
- }
+ public void enableSystemDataSync(int associationId, int flags) {
+ mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags);
}
@Override
- public void notifyDeviceDisappeared(int associationId) {
- if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId);
+ public void disableSystemDataSync(int associationId, int flags) {
+ mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags);
+ }
- 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 enablePermissionsSync(int associationId) {
+ mSystemDataTransferProcessor.enablePermissionsSync(associationId);
+ }
- mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
+ @Override
+ public void disablePermissionsSync(int associationId) {
+ mSystemDataTransferProcessor.disablePermissionsSync(associationId);
+ }
- 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
+ public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
+ return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
+ }
+
+ @Override
+ @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
+ public void notifySelfManagedDeviceAppeared(int associationId) {
+ notifySelfManagedDeviceAppeared_enforcePermission();
+
+ mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, true);
+ }
+
+ @Override
+ @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
+ public void notifySelfManagedDeviceDisappeared(int associationId) {
+ notifySelfManagedDeviceDisappeared_enforcePermission();
+
+ mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, false);
}
@Override
public boolean isCompanionApplicationBound(String packageName, int userId) {
- return mCompanionAppController.isCompanionApplicationBound(userId, packageName);
- }
-
- private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
- boolean active) throws RemoteException {
- if (DEBUG) {
- Log.i(TAG, "registerDevicePresenceListenerActive()"
- + " active=" + active
- + " deviceAddress=" + deviceAddress);
- }
- final int userId = getCallingUserId();
- enforceCallerIsSystemOr(userId, packageName);
-
- AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(
- userId, packageName, deviceAddress);
-
- if (association == null) {
- throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
- + " is not associated with device " + deviceAddress
- + " for user " + userId));
- }
-
- processDevicePresenceListener(association, userId, packageName, active);
- }
-
- private void processDevicePresenceListener(AssociationInfo association,
- int userId, String packageName, boolean active) {
- // If already at specified state, then no-op.
- if (active == association.isNotifyOnDeviceNearby()) {
- if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state.");
- return;
- }
-
- // AssociationInfo class is immutable: create a new AssociationInfo object with updated
- // flag.
- association = (new AssociationInfo.Builder(association))
- .setNotifyOnDeviceNearby(active)
- .build();
- // Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will
- // trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when
- // an application sets/unsets the mNotifyOnDeviceNearby flag.
- mAssociationStore.updateAssociation(association);
-
- int associationId = association.getId();
- // If device is already present, then trigger callback.
- if (active && mDevicePresenceMonitor.isDevicePresent(associationId)) {
- Slog.i(TAG, "Device is already present. Triggering callback.");
- if (mDevicePresenceMonitor.isBlePresent(associationId)
- || mDevicePresenceMonitor.isSimulatePresent(associationId)) {
- onDeviceAppearedInternal(associationId);
- onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED);
- } else if (mDevicePresenceMonitor.isBtConnected(associationId)) {
- onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED);
- }
- }
-
- // If last listener is unregistered, then unbind application.
- if (!active && !shouldBindPackage(userId, packageName)) {
- if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application.");
- mCompanionAppController.unbindCompanionApplication(userId, packageName);
- }
+ return mCompanionAppBinder.isCompanionApplicationBound(userId, packageName);
}
@Override
@@ -1070,7 +680,8 @@
}
final MacAddress macAddressObj = MacAddress.fromString(macAddress);
- createNewAssociation(userId, packageName, macAddressObj, null, null, false);
+ mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj,
+ null, null, null, false, null, null);
}
private void checkCanCallNotificationApi(String callingPackage, int userId) {
@@ -1099,9 +710,7 @@
@Override
public void setAssociationTag(int associationId, String tag) {
- AssociationInfo association = getAssociationWithCallerChecks(associationId);
- association = (new AssociationInfo.Builder(association)).setTag(tag).build();
- mAssociationStore.updateAssociation(association);
+ mAssociationRequestsProcessor.setAssociationTag(associationId, tag);
}
@Override
@@ -1124,7 +733,7 @@
@NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
@NonNull String[] args) {
return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this,
- mAssociationStore, mDevicePresenceMonitor, mTransportManager,
+ mAssociationStore, mDevicePresenceProcessor, mTransportManager,
mSystemDataTransferProcessor, mAssociationRequestsProcessor,
mBackupRestoreProcessor, mDisassociationProcessor)
.exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
@@ -1139,21 +748,13 @@
}
mAssociationStore.dump(out);
- mDevicePresenceMonitor.dump(out);
- mCompanionAppController.dump(out);
+ mDevicePresenceProcessor.dump(out);
+ mCompanionAppBinder.dump(out);
mTransportManager.dump(out);
mSystemDataTransferRequestStore.dump(out);
}
}
- void createNewAssociation(@UserIdInt int userId, @NonNull String packageName,
- @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
- @Nullable String deviceProfile, boolean isSelfManaged) {
- mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
- displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged,
- /* callback */ null, /* resultReceiver */ null);
- }
-
/**
* Update special access for the association's package
*/
@@ -1169,8 +770,6 @@
return;
}
- Slog.i(TAG, "Updating special access for package=[" + packageInfo.packageName + "]...");
-
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.RUN_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
@@ -1280,29 +879,6 @@
}
};
- private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
- new CompanionDevicePresenceMonitor.Callback() {
- @Override
- public void onDeviceAppeared(int associationId) {
- onDeviceAppearedInternal(associationId);
- }
-
- @Override
- public void onDeviceDisappeared(int associationId) {
- onDeviceDisappearedInternal(associationId);
- }
-
- @Override
- public void onDevicePresenceEvent(int associationId, int event) {
- onDevicePresenceEventInternal(associationId, event);
- }
-
- @Override
- public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
- onDevicePresenceEventByUuidInternal(uuid, event);
- }
- };
-
private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@Override
public void onPackageRemoved(String packageName, int uid) {
@@ -1315,7 +891,7 @@
}
@Override
- public void onPackageModified(String packageName) {
+ public void onPackageModified(@NonNull String packageName) {
onPackageModifiedInternal(getChangingUserId(), packageName);
}
@@ -1325,25 +901,15 @@
}
};
- private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) {
- final Map<String, Set<Integer>> copy = new HashMap<>();
-
- for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) {
- final Set<Integer> valueCopy = new HashSet<>(entry.getValue());
- copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy));
- }
-
- return Collections.unmodifiableMap(copy);
- }
-
private static <T> boolean containsEither(T[] array, T a, T b) {
return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
}
private class LocalService implements CompanionDeviceManagerServiceInternal {
+
@Override
public void removeInactiveSelfManagedAssociations() {
- CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations();
+ mDisassociationProcessor.removeIdleSelfManagedAssociations();
}
@Override
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
index cdf832f..9d1250d 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
@@ -27,8 +27,9 @@
* Companion Device Manager Local System Service Interface.
*/
public interface CompanionDeviceManagerServiceInternal {
+
/**
- * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations
+ * Remove idle self-managed associations.
*/
void removeInactiveSelfManagedAssociations();
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index a7a73cb..a789384 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -18,8 +18,6 @@
import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC;
-import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks;
-
import android.companion.AssociationInfo;
import android.companion.ContextSyncMessage;
import android.companion.Flags;
@@ -38,7 +36,7 @@
import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
import com.android.server.companion.datatransfer.contextsync.BitmapUtils;
import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.DevicePresenceProcessor;
import com.android.server.companion.presence.ObservableUuid;
import com.android.server.companion.transport.CompanionTransportManager;
@@ -51,7 +49,7 @@
private final CompanionDeviceManagerService mService;
private final DisassociationProcessor mDisassociationProcessor;
private final AssociationStore mAssociationStore;
- private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
+ private final DevicePresenceProcessor mDevicePresenceProcessor;
private final CompanionTransportManager mTransportManager;
private final SystemDataTransferProcessor mSystemDataTransferProcessor;
@@ -60,7 +58,7 @@
CompanionDeviceShellCommand(CompanionDeviceManagerService service,
AssociationStore associationStore,
- CompanionDevicePresenceMonitor devicePresenceMonitor,
+ DevicePresenceProcessor devicePresenceProcessor,
CompanionTransportManager transportManager,
SystemDataTransferProcessor systemDataTransferProcessor,
AssociationRequestsProcessor associationRequestsProcessor,
@@ -68,7 +66,7 @@
DisassociationProcessor disassociationProcessor) {
mService = service;
mAssociationStore = associationStore;
- mDevicePresenceMonitor = devicePresenceMonitor;
+ mDevicePresenceProcessor = devicePresenceProcessor;
mTransportManager = transportManager;
mSystemDataTransferProcessor = systemDataTransferProcessor;
mAssociationRequestsProcessor = associationRequestsProcessor;
@@ -85,7 +83,7 @@
if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) {
associationId = getNextIntArgRequired();
int event = getNextIntArgRequired();
- mDevicePresenceMonitor.simulateDeviceEvent(associationId, event);
+ mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
return 0;
}
@@ -97,7 +95,7 @@
ObservableUuid observableUuid = new ObservableUuid(
userId, ParcelUuid.fromString(uuid), packageName,
System.currentTimeMillis());
- mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event);
+ mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
return 0;
}
@@ -124,8 +122,9 @@
String address = getNextArgRequired();
String deviceProfile = getNextArg();
final MacAddress macAddress = MacAddress.fromString(address);
- mService.createNewAssociation(userId, packageName, macAddress,
- /* displayName= */ deviceProfile, deviceProfile, false);
+ mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
+ deviceProfile, deviceProfile, /* associatedDevice */ null, false,
+ /* callback */ null, /* resultReceiver */ null);
}
break;
@@ -134,8 +133,13 @@
final String packageName = getNextArgRequired();
final String address = getNextArgRequired();
final AssociationInfo association =
- mService.getAssociationWithCallerChecks(userId, packageName, address);
- mDisassociationProcessor.disassociate(association.getId());
+ mAssociationStore.getFirstAssociationByAddress(userId, packageName,
+ address);
+ if (association == null) {
+ out.println("Association doesn't exist.");
+ } else {
+ mDisassociationProcessor.disassociate(association.getId());
+ }
}
break;
@@ -144,9 +148,7 @@
final List<AssociationInfo> userAssociations =
mAssociationStore.getAssociationsByUser(userId);
for (AssociationInfo association : userAssociations) {
- if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
- mDisassociationProcessor.disassociate(association.getId());
- }
+ mDisassociationProcessor.disassociate(association.getId());
}
}
break;
@@ -157,12 +159,12 @@
case "simulate-device-appeared":
associationId = getNextIntArgRequired();
- mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0);
+ mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0);
break;
case "simulate-device-disappeared":
associationId = getNextIntArgRequired();
- mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1);
+ mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1);
break;
case "get-backup-payload": {
@@ -410,10 +412,9 @@
pw.println(" Remove an existing Association.");
pw.println(" disassociate-all USER_ID");
pw.println(" Remove all Associations for a user.");
- pw.println(" clear-association-memory-cache");
+ pw.println(" refresh-cache");
pw.println(" Clear the in-memory association cache and reload all association ");
- pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY.");
- pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+ pw.println(" information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
pw.println(" simulate-device-appeared ASSOCIATION_ID");
pw.println(" Make CDM act as if the given companion device has appeared.");
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index a02d9f9..a18776e 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -145,7 +145,8 @@
/**
* Handle incoming {@link AssociationRequest}s, sent via
- * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
+ * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest,
+ * IAssociationRequestCallback, String, int)}
*/
public void processNewAssociationRequest(@NonNull AssociationRequest request,
@NonNull String packageName, @UserIdInt int userId,
@@ -212,7 +213,8 @@
// 2b.4. Send the PendingIntent back to the app.
try {
callback.onAssociationPending(pendingIntent);
- } catch (RemoteException ignore) { }
+ } catch (RemoteException ignore) {
+ }
}
/**
@@ -252,7 +254,8 @@
// forward it back to the application via the callback.
try {
callback.onFailure(e.getMessage());
- } catch (RemoteException ignore) { }
+ } catch (RemoteException ignore) {
+ }
return;
}
@@ -322,7 +325,8 @@
* Enable system data sync.
*/
public void enableSystemDataSync(int associationId, int flags) {
- AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
AssociationInfo updated = (new AssociationInfo.Builder(association))
.setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build();
mAssociationStore.updateAssociation(updated);
@@ -332,12 +336,23 @@
* Disable system data sync.
*/
public void disableSystemDataSync(int associationId, int flags) {
- AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
AssociationInfo updated = (new AssociationInfo.Builder(association))
.setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build();
mAssociationStore.updateAssociation(updated);
}
+ /**
+ * Set association tag.
+ */
+ public void setAssociationTag(int associationId, String tag) {
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
+ association = (new AssociationInfo.Builder(association)).setTag(tag).build();
+ mAssociationStore.updateAssociation(association);
+ }
+
private void sendCallbackAndFinish(@Nullable AssociationInfo association,
@Nullable IAssociationRequestCallback callback,
@Nullable ResultReceiver resultReceiver) {
@@ -396,14 +411,14 @@
// 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 edebb55..ae2b708 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationStore.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java
@@ -18,6 +18,7 @@
import static com.android.server.companion.utils.MetricUtils.logCreateAssociation;
import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation;
+import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -26,6 +27,7 @@
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.companion.IOnAssociationsChangedListener;
+import android.content.Context;
import android.content.pm.UserInfo;
import android.net.MacAddress;
import android.os.Binder;
@@ -57,21 +59,22 @@
@SuppressLint("LongLogTag")
public class AssociationStore {
- @IntDef(prefix = { "CHANGE_TYPE_" }, value = {
+ @IntDef(prefix = {"CHANGE_TYPE_"}, value = {
CHANGE_TYPE_ADDED,
CHANGE_TYPE_REMOVED,
CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface ChangeType {}
+ public @interface ChangeType {
+ }
public static final int CHANGE_TYPE_ADDED = 0;
public static final int CHANGE_TYPE_REMOVED = 1;
public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;
- /** Listener for any changes to associations. */
+ /** Listener for any changes to associations. */
public interface OnChangeListener {
/**
* Called when there are association changes.
@@ -100,25 +103,30 @@
/**
* Called when an association is added.
*/
- default void onAssociationAdded(AssociationInfo association) {}
+ default void onAssociationAdded(AssociationInfo association) {
+ }
/**
* Called when an association is removed.
*/
- default void onAssociationRemoved(AssociationInfo association) {}
+ default void onAssociationRemoved(AssociationInfo association) {
+ }
/**
* Called when an association is updated.
*/
- default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {}
+ default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {
+ }
}
private static final String TAG = "CDM_AssociationStore";
- private final Object mLock = new Object();
-
+ private final Context mContext;
+ private final UserManager mUserManager;
+ private final AssociationDiskStore mDiskStore;
private final ExecutorService mExecutor;
+ private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mPersisted = false;
@GuardedBy("mLock")
@@ -132,10 +140,9 @@
private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners =
new RemoteCallbackList<>();
- private final UserManager mUserManager;
- private final AssociationDiskStore mDiskStore;
-
- public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) {
+ public AssociationStore(Context context, UserManager userManager,
+ AssociationDiskStore diskStore) {
+ mContext = context;
mUserManager = userManager;
mDiskStore = diskStore;
mExecutor = Executors.newSingleThreadExecutor();
@@ -202,7 +209,7 @@
synchronized (mLock) {
if (mIdToAssociationMap.containsKey(id)) {
- Slog.e(TAG, "Association with id=[" + id + "] already exists.");
+ Slog.e(TAG, "Association id=[" + id + "] already exists.");
return;
}
@@ -449,6 +456,26 @@
}
/**
+ * 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 ec897791..acf683d 100644
--- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
@@ -22,6 +22,8 @@
import static com.android.internal.util.CollectionUtils.any;
import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation;
+import static java.util.concurrent.TimeUnit.DAYS;
+
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
@@ -30,21 +32,27 @@
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.util.Slog;
-import com.android.server.companion.CompanionApplicationController;
import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.CompanionAppBinder;
+import com.android.server.companion.presence.DevicePresenceProcessor;
import com.android.server.companion.transport.CompanionTransportManager;
/**
- * A class response for Association removal.
+ * This class responsible for disassociation.
*/
@SuppressLint("LongLogTag")
public class DisassociationProcessor {
private static final String TAG = "CDM_DisassociationProcessor";
+
+ private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
+ "debug.cdm.cdmservice.removal_time_window";
+ private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
+
@NonNull
private final Context mContext;
@NonNull
@@ -52,11 +60,11 @@
@NonNull
private final PackageManagerInternal mPackageManagerInternal;
@NonNull
- private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
+ private final DevicePresenceProcessor mDevicePresenceMonitor;
@NonNull
private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
@NonNull
- private final CompanionApplicationController mCompanionAppController;
+ private final CompanionAppBinder mCompanionAppController;
@NonNull
private final CompanionTransportManager mTransportManager;
private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
@@ -66,8 +74,8 @@
@NonNull ActivityManager activityManager,
@NonNull AssociationStore associationStore,
@NonNull PackageManagerInternal packageManager,
- @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor,
- @NonNull CompanionApplicationController applicationController,
+ @NonNull DevicePresenceProcessor devicePresenceMonitor,
+ @NonNull CompanionAppBinder applicationController,
@NonNull SystemDataTransferRequestStore systemDataTransferRequestStore,
@NonNull CompanionTransportManager companionTransportManager) {
mContext = context;
@@ -89,11 +97,7 @@
public void disassociate(int id) {
Slog.i(TAG, "Disassociating id=[" + id + "]...");
- final AssociationInfo association = mAssociationStore.getAssociationById(id);
- if (association == null) {
- Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist.");
- return;
- }
+ final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id);
final int userId = association.getUserId();
final String packageName = association.getPackageName();
@@ -118,12 +122,12 @@
return;
}
- // Association cleanup.
- mAssociationStore.removeAssociation(association.getId());
- mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id);
-
// Detach transport if exists
- mTransportManager.detachSystemDataTransport(packageName, userId, id);
+ mTransportManager.detachSystemDataTransport(id);
+
+ // Association cleanup.
+ mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id);
+ mAssociationStore.removeAssociation(association.getId());
// If role is not in use by other associations, revoke the role.
// Do not need to remove the system role since it was pre-granted by the system.
@@ -143,10 +147,28 @@
it -> it.isNotifyOnDeviceNearby()
&& mDevicePresenceMonitor.isDevicePresent(it.getId()));
if (!shouldStayBound) {
- mCompanionAppController.unbindCompanionApplication(userId, packageName);
+ mCompanionAppController.unbindCompanionApp(userId, packageName);
}
}
+ /**
+ * @deprecated Use {@link #disassociate(int)} instead.
+ */
+ @Deprecated
+ public void disassociate(int userId, String packageName, String macAddress) {
+ AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+ packageName, macAddress);
+
+ if (association == null) {
+ throw new IllegalArgumentException(
+ "Association for mac address=[" + macAddress + "] doesn't exist");
+ }
+
+ mAssociationStore.getAssociationWithCallerChecks(association.getId());
+
+ disassociate(association.getId());
+ }
+
@SuppressLint("MissingPermission")
private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
return Binder.withCleanCallingIdentity(() -> {
@@ -163,7 +185,7 @@
() -> mActivityManager.addOnUidImportanceListener(
mOnPackageVisibilityChangeListener,
ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE));
- } catch (IllegalArgumentException e) {
+ } catch (IllegalArgumentException e) {
Slog.e(TAG, "Failed to start listening to uid importance changes.");
}
}
@@ -179,6 +201,34 @@
}
/**
+ * Remove idle self-managed associations.
+ */
+ public void removeIdleSelfManagedAssociations() {
+ Slog.i(TAG, "Removing idle self-managed associations.");
+
+ final long currentTime = System.currentTimeMillis();
+ long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
+ if (removalWindow <= 0) {
+ // 0 or negative values indicate that the sysprop was never set or should be ignored.
+ removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
+ }
+
+ for (AssociationInfo association : mAssociationStore.getAssociations()) {
+ if (!association.isSelfManaged()) continue;
+
+ final boolean isInactive =
+ currentTime - association.getLastTimeConnectedMs() >= removalWindow;
+ if (!isInactive) continue;
+
+ final int id = association.getId();
+
+ Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString()
+ + "].");
+ disassociate(id);
+ }
+ }
+
+ /**
* An OnUidImportanceListener class which watches the importance of the packages.
* In this class, we ONLY interested in the importance of the running process is greater than
* {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}.
diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
index f287315..b509e71 100644
--- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
+++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
@@ -30,7 +30,7 @@
import com.android.server.companion.CompanionDeviceManagerServiceInternal;
/**
- * A Job Service responsible for clean up idle self-managed associations.
+ * A Job Service responsible for clean up self-managed associations if it's idle for 90 days.
*
* The job will be executed only if the device is charging and in idle mode due to the application
* will be killed if association/role are revoked. See {@link DisassociationProcessor}
@@ -45,10 +45,10 @@
@Override
public boolean onStartJob(final JobParameters params) {
Slog.i(TAG, "Execute the Association Removal job");
- // Special policy for selfManaged that need to revoke associations if the device
- // does not connect for 90 days.
+
LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
.removeInactiveSelfManagedAssociations();
+
jobFinished(params, false);
return true;
}
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index c5ca0bf..9069689 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -31,7 +31,6 @@
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.companion.AssociationInfo;
-import android.companion.DeviceNotAssociatedException;
import android.companion.IOnMessageReceivedListener;
import android.companion.ISystemDataTransferCallback;
import android.companion.datatransfer.PermissionSyncRequest;
@@ -56,7 +55,6 @@
import com.android.server.companion.association.AssociationStore;
import com.android.server.companion.transport.CompanionTransportManager;
import com.android.server.companion.utils.PackageUtils;
-import com.android.server.companion.utils.PermissionsUtils;
import java.util.List;
import java.util.concurrent.ExecutorService;
@@ -120,28 +118,10 @@
}
/**
- * Resolve the requested association, throwing if the caller doesn't have
- * adequate permissions.
- */
- @NonNull
- private AssociationInfo resolveAssociation(String packageName, int userId,
- int associationId) {
- AssociationInfo association = mAssociationStore.getAssociationById(associationId);
- association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association);
- if (association == null) {
- throw new DeviceNotAssociatedException("Association "
- + associationId + " is not associated with the app " + packageName
- + " for user " + userId);
- }
- return association;
- }
-
- /**
* Return whether the user has consented to the permission transfer for the association.
*/
- public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId,
- int associationId) {
- resolveAssociation(packageName, userId, associationId);
+ public boolean isPermissionTransferUserConsented(int associationId) {
+ mAssociationStore.getAssociationWithCallerChecks(associationId);
PermissionSyncRequest request = getPermissionSyncRequest(associationId);
if (request == null) {
@@ -167,7 +147,8 @@
return null;
}
- final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
+ final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
+ "] associationId [" + associationId + "]");
@@ -207,7 +188,7 @@
Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName
+ "] userId [" + userId + "] associationId [" + associationId + "]");
- final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
+ mAssociationStore.getAssociationWithCallerChecks(associationId);
// Check if the request has been consented by the user.
PermissionSyncRequest request = getPermissionSyncRequest(associationId);
@@ -239,24 +220,20 @@
* Enable perm sync for the association
*/
public void enablePermissionsSync(int associationId) {
- Binder.withCleanCallingIdentity(() -> {
- int userId = mAssociationStore.getAssociationById(associationId).getUserId();
- PermissionSyncRequest request = new PermissionSyncRequest(associationId);
- request.setUserConsented(true);
- mSystemDataTransferRequestStore.writeRequest(userId, request);
- });
+ int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
+ PermissionSyncRequest request = new PermissionSyncRequest(associationId);
+ request.setUserConsented(true);
+ mSystemDataTransferRequestStore.writeRequest(userId, request);
}
/**
* Disable perm sync for the association
*/
public void disablePermissionsSync(int associationId) {
- Binder.withCleanCallingIdentity(() -> {
- int userId = mAssociationStore.getAssociationById(associationId).getUserId();
- PermissionSyncRequest request = new PermissionSyncRequest(associationId);
- request.setUserConsented(false);
- mSystemDataTransferRequestStore.writeRequest(userId, request);
- });
+ int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
+ PermissionSyncRequest request = new PermissionSyncRequest(associationId);
+ request.setUserConsented(false);
+ mSystemDataTransferRequestStore.writeRequest(userId, request);
}
/**
@@ -264,18 +241,17 @@
*/
@Nullable
public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
- return Binder.withCleanCallingIdentity(() -> {
- int userId = mAssociationStore.getAssociationById(associationId).getUserId();
- List<SystemDataTransferRequest> requests =
- mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
- associationId);
- for (SystemDataTransferRequest request : requests) {
- if (request instanceof PermissionSyncRequest) {
- return (PermissionSyncRequest) request;
- }
+ int userId = mAssociationStore.getAssociationWithCallerChecks(associationId)
+ .getUserId();
+ List<SystemDataTransferRequest> requests =
+ mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
+ associationId);
+ for (SystemDataTransferRequest request : requests) {
+ if (request instanceof PermissionSyncRequest) {
+ return (PermissionSyncRequest) request;
}
- return null;
- });
+ }
+ return null;
}
/**
diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
index c89ce11..9c37881 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.CompanionDevicePresenceMonitor.DEBUG;
+import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG;
import static com.android.server.companion.utils.Utils.btDeviceToString;
import static java.util.Objects.requireNonNull;
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index cb363a7..2d345c4 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.CompanionDevicePresenceMonitor.DEBUG;
+import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG;
import static com.android.server.companion.utils.Utils.btDeviceToString;
import android.annotation.NonNull;
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java
new file mode 100644
index 0000000..b6348ea
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.presence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.UserIdInt;
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.PerUser;
+import com.android.server.companion.CompanionDeviceManagerService;
+import com.android.server.companion.utils.PackageUtils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages communication with companion applications via
+ * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
+ * the services, maintaining the connection (the binding), and invoking callback methods such as
+ * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
+ * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
+ * application process.
+ *
+ * <p>
+ * The following is the list of the APIs provided by {@link CompanionAppBinder} (to be
+ * utilized by {@link CompanionDeviceManagerService}):
+ * <ul>
+ * <li> {@link #bindCompanionApp(int, String, boolean, CompanionServiceConnector.Listener)}
+ * <li> {@link #unbindCompanionApp(int, String)}
+ * <li> {@link #isCompanionApplicationBound(int, String)}
+ * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
+ * </ul>
+ *
+ * @see CompanionDeviceService
+ * @see android.companion.ICompanionDeviceService
+ * @see CompanionServiceConnector
+ */
+@SuppressLint("LongLogTag")
+public class CompanionAppBinder {
+ private static final String TAG = "CDM_CompanionAppBinder";
+
+ private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final CompanionServicesRegister mCompanionServicesRegister;
+
+ @NonNull
+ @GuardedBy("mBoundCompanionApplications")
+ private final Map<Pair<Integer, String>, List<CompanionServiceConnector>>
+ mBoundCompanionApplications;
+ @NonNull
+ @GuardedBy("mScheduledForRebindingCompanionApplications")
+ private final Set<Pair<Integer, String>> mScheduledForRebindingCompanionApplications;
+
+ public CompanionAppBinder(@NonNull Context context) {
+ mContext = context;
+ mCompanionServicesRegister = new CompanionServicesRegister();
+ mBoundCompanionApplications = new HashMap<>();
+ mScheduledForRebindingCompanionApplications = new HashSet<>();
+ }
+
+ /**
+ * On package changed.
+ */
+ public void onPackagesChanged(@UserIdInt int userId) {
+ mCompanionServicesRegister.invalidate(userId);
+ }
+
+ /**
+ * CDM binds to the companion app.
+ */
+ public void bindCompanionApp(@UserIdInt int userId, @NonNull String packageName,
+ boolean isSelfManaged, CompanionServiceConnector.Listener listener) {
+ Slog.i(TAG, "Binding user=[" + userId + "], package=[" + packageName + "], isSelfManaged=["
+ + isSelfManaged + "]...");
+
+ final List<ComponentName> companionServices =
+ mCompanionServicesRegister.forPackage(userId, packageName);
+ if (companionServices.isEmpty()) {
+ Slog.e(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
+ + "eligible CompanionDeviceService not found.\n"
+ + "A CompanionDeviceService should declare an intent-filter for "
+ + "\"android.companion.CompanionDeviceService\" action and require "
+ + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
+ return;
+ }
+
+ final List<CompanionServiceConnector> serviceConnectors = new ArrayList<>();
+ synchronized (mBoundCompanionApplications) {
+ if (mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) {
+ Slog.w(TAG, "The package is ALREADY bound.");
+ return;
+ }
+
+ for (int i = 0; i < companionServices.size(); i++) {
+ boolean isPrimary = i == 0;
+ serviceConnectors.add(CompanionServiceConnector.newInstance(mContext, userId,
+ companionServices.get(i), isSelfManaged, isPrimary));
+ }
+
+ mBoundCompanionApplications.put(new Pair<>(userId, packageName), serviceConnectors);
+ }
+
+ // Set listeners for both Primary and Secondary connectors.
+ for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+ serviceConnector.setListener(listener);
+ }
+
+ // Now "bind" all the connectors: the primary one and the rest of them.
+ for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+ serviceConnector.connect();
+ }
+ }
+
+ /**
+ * CDM unbinds the companion app.
+ */
+ public void unbindCompanionApp(@UserIdInt int userId, @NonNull String packageName) {
+ Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]...");
+
+ final List<CompanionServiceConnector> serviceConnectors;
+
+ synchronized (mBoundCompanionApplications) {
+ serviceConnectors = mBoundCompanionApplications.remove(new Pair<>(userId, packageName));
+ }
+
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+ }
+
+ if (serviceConnectors == null) {
+ Slog.e(TAG, "The package is not bound.");
+ return;
+ }
+
+ for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+ serviceConnector.postUnbind();
+ }
+ }
+
+ /**
+ * @return whether the companion application is bound now.
+ */
+ public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
+ synchronized (mBoundCompanionApplications) {
+ return mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName));
+ }
+ }
+
+ /**
+ * Remove bound apps for package.
+ */
+ public void removePackage(int userId, String packageName) {
+ synchronized (mBoundCompanionApplications) {
+ mBoundCompanionApplications.remove(new Pair<>(userId, packageName));
+ }
+
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+ }
+ }
+
+ /**
+ * Schedule rebinding for the package.
+ */
+ public void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
+ CompanionServiceConnector serviceConnector) {
+ Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
+
+ if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
+ Slog.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
+ + serviceConnector.getComponentName());
+ return;
+ }
+
+ if (serviceConnector.isPrimary()) {
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ mScheduledForRebindingCompanionApplications.add(new Pair<>(userId, packageName));
+ }
+ }
+
+ // Rebinding in 10 seconds.
+ Handler.getMain().postDelayed(() ->
+ onRebindingCompanionApplicationTimeout(userId, packageName,
+ serviceConnector),
+ REBIND_TIMEOUT);
+ }
+
+ private boolean isRebindingCompanionApplicationScheduled(
+ @UserIdInt int userId, @NonNull String packageName) {
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ return mScheduledForRebindingCompanionApplications.contains(
+ new Pair<>(userId, packageName));
+ }
+ }
+
+ private void onRebindingCompanionApplicationTimeout(
+ @UserIdInt int userId, @NonNull String packageName,
+ @NonNull CompanionServiceConnector serviceConnector) {
+ // Re-mark the application is bound.
+ if (serviceConnector.isPrimary()) {
+ synchronized (mBoundCompanionApplications) {
+ if (!mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) {
+ List<CompanionServiceConnector> serviceConnectors =
+ Collections.singletonList(serviceConnector);
+ mBoundCompanionApplications.put(new Pair<>(userId, packageName),
+ serviceConnectors);
+ }
+ }
+
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+ }
+ }
+
+ serviceConnector.connect();
+ }
+
+ /**
+ * Dump bound apps.
+ */
+ public void dump(@NonNull PrintWriter out) {
+ out.append("Companion Device Application Controller: \n");
+
+ synchronized (mBoundCompanionApplications) {
+ out.append(" Bound Companion Applications: ");
+ if (mBoundCompanionApplications.isEmpty()) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (Map.Entry<Pair<Integer, String>, List<CompanionServiceConnector>> entry :
+ mBoundCompanionApplications.entrySet()) {
+ out.append("<u").append(String.valueOf(entry.getKey().first)).append(", ")
+ .append(entry.getKey().second).append(">");
+ for (CompanionServiceConnector serviceConnector : entry.getValue()) {
+ out.append(", isPrimary=").append(
+ String.valueOf(serviceConnector.isPrimary()));
+ }
+ }
+ }
+ }
+
+ out.append(" Companion Applications Scheduled For Rebinding: ");
+ synchronized (mScheduledForRebindingCompanionApplications) {
+ if (mScheduledForRebindingCompanionApplications.isEmpty()) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (Pair<Integer, String> app : mScheduledForRebindingCompanionApplications) {
+ out.append("<u").append(String.valueOf(app.first)).append(", ")
+ .append(app.second).append(">");
+ }
+ }
+ }
+ }
+
+ @Nullable
+ CompanionServiceConnector getPrimaryServiceConnector(
+ @UserIdInt int userId, @NonNull String packageName) {
+ final List<CompanionServiceConnector> connectors;
+ synchronized (mBoundCompanionApplications) {
+ connectors = mBoundCompanionApplications.get(new Pair<>(userId, packageName));
+ }
+ return connectors != null ? connectors.get(0) : null;
+ }
+
+ private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
+ @Override
+ public synchronized @NonNull Map<String, List<ComponentName>> forUser(
+ @UserIdInt int userId) {
+ return super.forUser(userId);
+ }
+
+ synchronized @NonNull List<ComponentName> forPackage(
+ @UserIdInt int userId, @NonNull String packageName) {
+ return forUser(userId).getOrDefault(packageName, Collections.emptyList());
+ }
+
+ synchronized void invalidate(@UserIdInt int userId) {
+ remove(userId);
+ }
+
+ @Override
+ protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
+ return PackageUtils.getCompanionServicesForUser(mContext, userId);
+ }
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
deleted file mode 100644
index 7a1a83f..0000000
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion.presence;
-
-import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
-import static android.os.Process.ROOT_UID;
-import static android.os.Process.SHELL_UID;
-
-import android.annotation.NonNull;
-import android.annotation.SuppressLint;
-import android.annotation.TestApi;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.companion.AssociationInfo;
-import android.content.Context;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.ParcelUuid;
-import android.os.UserManager;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.companion.association.AssociationStore;
-
-import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Class responsible for monitoring companion devices' "presence" status (i.e.
- * connected/disconnected for Bluetooth devices; nearby or not for BLE devices).
- *
- * <p>
- * Should only be used by
- * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
- * to which it provides the following API:
- * <ul>
- * <li> {@link #onSelfManagedDeviceConnected(int)}
- * <li> {@link #onSelfManagedDeviceDisconnected(int)}
- * <li> {@link #isDevicePresent(int)}
- * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)}
- * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)}
- * <li> {@link Callback#onDevicePresenceEvent(int, int)}}
- * </ul>
- */
-@SuppressLint("LongLogTag")
-public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener,
- BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback {
- static final boolean DEBUG = false;
- private static final String TAG = "CDM_CompanionDevicePresenceMonitor";
-
- /** Callback for notifying about changes to status of companion devices. */
- public interface Callback {
- /** Invoked when companion device is found nearby or connects. */
- void onDeviceAppeared(int associationId);
-
- /** Invoked when a companion device no longer seen nearby or disconnects. */
- void onDeviceDisappeared(int associationId);
-
- /** Invoked when device has corresponding event changes. */
- void onDevicePresenceEvent(int associationId, int event);
-
- /** Invoked when device has corresponding event changes base on the UUID */
- void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
- }
-
- private final @NonNull AssociationStore mAssociationStore;
- private final @NonNull ObservableUuidStore mObservableUuidStore;
- private final @NonNull Callback mCallback;
- private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
- private final @NonNull BleCompanionDeviceScanner mBleScanner;
-
- // NOTE: Same association may appear in more than one of the following sets at the same time.
- // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
- // companion applications, while at the same be connected via BT, or detected nearby by BLE
- // scanner)
- private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>();
- private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>();
- private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
- private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
- @GuardedBy("mBtDisconnectedDevices")
- private final @NonNull Set<Integer> mBtDisconnectedDevices = new HashSet<>();
-
- // A map to track device presence within 10 seconds of Bluetooth disconnection.
- // The key is the association ID, and the boolean value indicates if the device
- // was detected again within that time frame.
- @GuardedBy("mBtDisconnectedDevices")
- private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence =
- new SparseBooleanArray();
-
- // Tracking "simulated" presence. Used for debugging and testing only.
- private final @NonNull Set<Integer> mSimulated = new HashSet<>();
- private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper =
- new SimulatedDevicePresenceSchedulerHelper();
-
- private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
- new BleDeviceDisappearedScheduler();
-
- public CompanionDevicePresenceMonitor(UserManager userManager,
- @NonNull AssociationStore associationStore,
- @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
- mAssociationStore = associationStore;
- mObservableUuidStore = observableUuidStore;
- mCallback = callback;
- mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
- associationStore, mObservableUuidStore,
- /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
- mBleScanner = new BleCompanionDeviceScanner(associationStore,
- /* BleCompanionDeviceScanner.Callback */ this);
- }
-
- /** Initialize {@link CompanionDevicePresenceMonitor} */
- public void init(Context context) {
- if (DEBUG) Log.i(TAG, "init()");
-
- final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
- if (btAdapter != null) {
- mBtConnectionListener.init(btAdapter);
- mBleScanner.init(context, btAdapter);
- } else {
- Log.w(TAG, "BluetoothAdapter is NOT available.");
- }
-
- mAssociationStore.registerLocalListener(this);
- }
-
- /**
- * @return current connected UUID devices.
- */
- public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
- return mConnectedUuidDevices;
- }
-
- /**
- * Remove current connected UUID device.
- */
- public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
- mConnectedUuidDevices.remove(uuid);
- }
-
- /**
- * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
- * or devices is connected (for Bluetooth); or reported (by the application) to be
- * nearby (for "self-managed" associations).
- */
- public boolean isDevicePresent(int associationId) {
- return mReportedSelfManagedDevices.contains(associationId)
- || mConnectedBtDevices.contains(associationId)
- || mNearbyBleDevices.contains(associationId)
- || mSimulated.contains(associationId);
- }
-
- /**
- * @return whether the current uuid to be observed is present.
- */
- public boolean isDeviceUuidPresent(ParcelUuid uuid) {
- return mConnectedUuidDevices.contains(uuid);
- }
-
- /**
- * @return whether the current device is BT connected and had already reported to the app.
- */
-
- public boolean isBtConnected(int associationId) {
- return mConnectedBtDevices.contains(associationId);
- }
-
- /**
- * @return whether the current device in BLE range and had already reported to the app.
- */
- public boolean isBlePresent(int associationId) {
- return mNearbyBleDevices.contains(associationId);
- }
-
- /**
- * @return whether the current device had been already reported by the simulator.
- */
- public boolean isSimulatePresent(int associationId) {
- return mSimulated.contains(associationId);
- }
-
- /**
- * Marks a "self-managed" device as connected.
- *
- * <p>
- * Must ONLY be invoked by the
- * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
- * when an application invokes
- * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()}
- */
- public void onSelfManagedDeviceConnected(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
- associationId, EVENT_SELF_MANAGED_APPEARED);
- }
-
- /**
- * Marks a "self-managed" device as disconnected.
- *
- * <p>
- * Must ONLY be invoked by the
- * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
- * when an application invokes
- * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()}
- */
- public void onSelfManagedDeviceDisconnected(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
- associationId, EVENT_SELF_MANAGED_DISAPPEARED);
- }
-
- /**
- * Marks a "self-managed" device as disconnected when binderDied.
- */
- public void onSelfManagedDeviceReporterBinderDied(int associationId) {
- onDevicePresenceEvent(mReportedSelfManagedDevices,
- associationId, EVENT_SELF_MANAGED_DISAPPEARED);
- }
-
- @Override
- public void onBluetoothCompanionDeviceConnected(int associationId) {
- synchronized (mBtDisconnectedDevices) {
- // A device is considered reconnected within 10 seconds if a pending BLE lost report is
- // followed by a detected Bluetooth connection.
- boolean isReconnected = mBtDisconnectedDevices.contains(associationId);
- if (isReconnected) {
- Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s.");
- mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
- }
-
- Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
- + "associationId( " + associationId + " )");
- onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
-
- // Stop the BLE scan if all devices report BT connected status and BLE was present.
- if (canStopBleScan()) {
- mBleScanner.stopScanIfNeeded();
- }
-
- }
- }
-
- @Override
- public void onBluetoothCompanionDeviceDisconnected(int associationId) {
- Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
- + "associationId( " + associationId + " )");
- // Start BLE scanning when the device is disconnected.
- mBleScanner.startScan();
-
- onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
- // If current device is BLE present but BT is disconnected , means it will be
- // potentially out of range later. Schedule BLE disappeared callback.
- if (isBlePresent(associationId)) {
- synchronized (mBtDisconnectedDevices) {
- mBtDisconnectedDevices.add(associationId);
- }
- mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId);
- }
- }
-
- @Override
- public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
- final ParcelUuid parcelUuid = uuid.getUuid();
-
- switch(event) {
- case EVENT_BT_CONNECTED:
- boolean added = mConnectedUuidDevices.add(parcelUuid);
-
- if (!added) {
- Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as "
- + "present by this event=" + event);
- }
-
- break;
- case EVENT_BT_DISCONNECTED:
- final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
-
- if (!removed) {
- Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported "
- + "as present by this event= " + event);
-
- return;
- }
-
- break;
- }
-
- mCallback.onDevicePresenceEventByUuid(uuid, event);
- }
-
-
- @Override
- public void onBleCompanionDeviceFound(int associationId) {
- onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
- synchronized (mBtDisconnectedDevices) {
- final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
- if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) {
- mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
- }
- }
- }
-
- @Override
- public void onBleCompanionDeviceLost(int associationId) {
- onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
- }
-
- /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
- @TestApi
- public void simulateDeviceEvent(int associationId, int event) {
- // IMPORTANT: this API should only be invoked via the
- // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
- // make this call are SHELL and ROOT.
- // No other caller (including SYSTEM!) should be allowed.
- enforceCallerShellOrRoot();
- // Make sure the association exists.
- enforceAssociationExists(associationId);
-
- switch (event) {
- case EVENT_BLE_APPEARED:
- simulateDeviceAppeared(associationId, event);
- break;
- case EVENT_BT_CONNECTED:
- onBluetoothCompanionDeviceConnected(associationId);
- break;
- case EVENT_BLE_DISAPPEARED:
- simulateDeviceDisappeared(associationId, event);
- break;
- case EVENT_BT_DISCONNECTED:
- onBluetoothCompanionDeviceDisconnected(associationId);
- break;
- default:
- throw new IllegalArgumentException("Event: " + event + "is not supported");
- }
- }
-
- /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
- @TestApi
- public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
- // IMPORTANT: this API should only be invoked via the
- // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to
- // make this call are SHELL and ROOT.
- // No other caller (including SYSTEM!) should be allowed.
- enforceCallerShellOrRoot();
- onDevicePresenceEventByUuid(uuid, event);
- }
-
- private void simulateDeviceAppeared(int associationId, int state) {
- onDevicePresenceEvent(mSimulated, associationId, state);
- mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
- }
-
- private void simulateDeviceDisappeared(int associationId, int state) {
- mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
- onDevicePresenceEvent(mSimulated, associationId, state);
- }
-
- private void enforceAssociationExists(int associationId) {
- if (mAssociationStore.getAssociationById(associationId) == null) {
- throw new IllegalArgumentException(
- "Association with id " + associationId + " does not exist.");
- }
- }
-
- private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
- int associationId, int event) {
- Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event);
-
- switch (event) {
- case EVENT_BLE_APPEARED:
- synchronized (mBtDisconnectedDevices) {
- // If a BLE device is detected within 10 seconds after BT is disconnected,
- // flag it as BLE is present.
- if (mBtDisconnectedDevices.contains(associationId)) {
- Slog.i(TAG, "Device ( " + associationId + " ) is present,"
- + " do not need to send the callback with event ( "
- + EVENT_BLE_APPEARED + " ).");
- mBtDisconnectedDevicesBlePresence.append(associationId, true);
- }
- }
- case EVENT_BT_CONNECTED:
- case EVENT_SELF_MANAGED_APPEARED:
- final boolean added = presentDevicesForSource.add(associationId);
-
- if (!added) {
- Slog.w(TAG, "Association with id "
- + associationId + " is ALREADY reported as "
- + "present by this source, event=" + event);
- }
-
- mCallback.onDeviceAppeared(associationId);
-
- break;
- case EVENT_BLE_DISAPPEARED:
- case EVENT_BT_DISCONNECTED:
- case EVENT_SELF_MANAGED_DISAPPEARED:
- final boolean removed = presentDevicesForSource.remove(associationId);
-
- if (!removed) {
- Slog.w(TAG, "Association with id " + associationId + " was NOT reported "
- + "as present by this source, event= " + event);
-
- return;
- }
-
- mCallback.onDeviceDisappeared(associationId);
-
- break;
- default:
- Slog.e(TAG, "Event: " + event + " is not supported");
- return;
- }
-
- mCallback.onDevicePresenceEvent(associationId, event);
- }
-
- /**
- * Implements
- * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
- */
- @Override
- public void onAssociationRemoved(@NonNull AssociationInfo association) {
- final int id = association.getId();
- if (DEBUG) {
- Log.i(TAG, "onAssociationRemoved() id=" + id);
- Log.d(TAG, " > association=" + association);
- }
-
- mConnectedBtDevices.remove(id);
- mNearbyBleDevices.remove(id);
- mReportedSelfManagedDevices.remove(id);
- mSimulated.remove(id);
- mBtDisconnectedDevices.remove(id);
- mBtDisconnectedDevicesBlePresence.delete(id);
-
- // Do NOT call mCallback.onDeviceDisappeared()!
- // CompanionDeviceManagerService will know that the association is removed, and will do
- // what's needed.
- }
-
- /**
- * Return a set of devices that pending to report connectivity
- */
- public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
- synchronized (mBtConnectionListener.mPendingConnectedDevices) {
- return mBtConnectionListener.mPendingConnectedDevices;
- }
- }
-
- private static void enforceCallerShellOrRoot() {
- final int callingUid = Binder.getCallingUid();
- if (callingUid == SHELL_UID || callingUid == ROOT_UID) return;
-
- throw new SecurityException("Caller is neither Shell nor Root");
- }
-
- /**
- * The BLE scan can be only stopped if all the devices have been reported
- * BT connected and BLE presence and are not pending to report BLE lost.
- */
- private boolean canStopBleScan() {
- for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) {
- int id = ai.getId();
- synchronized (mBtDisconnectedDevices) {
- if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id)
- && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) {
- Slog.i(TAG, "The BLE scan cannot be stopped, "
- + "device( " + id + " ) is not yet connected "
- + "OR the BLE is not current present Or is pending to report BLE lost");
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Dumps system information about devices that are marked as "present".
- */
- public void dump(@NonNull PrintWriter out) {
- out.append("Companion Device Present: ");
- if (mConnectedBtDevices.isEmpty()
- && mNearbyBleDevices.isEmpty()
- && mReportedSelfManagedDevices.isEmpty()) {
- out.append("<empty>\n");
- return;
- } else {
- out.append("\n");
- }
-
- out.append(" Connected Bluetooth Devices: ");
- if (mConnectedBtDevices.isEmpty()) {
- out.append("<empty>\n");
- } else {
- out.append("\n");
- for (int associationId : mConnectedBtDevices) {
- AssociationInfo a = mAssociationStore.getAssociationById(associationId);
- out.append(" ").append(a.toShortString()).append('\n');
- }
- }
-
- out.append(" Nearby BLE Devices: ");
- if (mNearbyBleDevices.isEmpty()) {
- out.append("<empty>\n");
- } else {
- out.append("\n");
- for (int associationId : mNearbyBleDevices) {
- AssociationInfo a = mAssociationStore.getAssociationById(associationId);
- out.append(" ").append(a.toShortString()).append('\n');
- }
- }
-
- out.append(" Self-Reported Devices: ");
- if (mReportedSelfManagedDevices.isEmpty()) {
- out.append("<empty>\n");
- } else {
- out.append("\n");
- for (int associationId : mReportedSelfManagedDevices) {
- AssociationInfo a = mAssociationStore.getAssociationById(associationId);
- out.append(" ").append(a.toShortString()).append('\n');
- }
- }
- }
-
- private class SimulatedDevicePresenceSchedulerHelper extends Handler {
- SimulatedDevicePresenceSchedulerHelper() {
- super(Looper.getMainLooper());
- }
-
- void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
- // First, unschedule if it was scheduled previously.
- if (hasMessages(/* what */ associationId)) {
- removeMessages(/* what */ associationId);
- }
-
- sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */);
- }
-
- void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
- removeMessages(/* what */ associationId);
- }
-
- @Override
- public void handleMessage(@NonNull Message msg) {
- final int associationId = msg.what;
- if (mSimulated.contains(associationId)) {
- onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
- }
- }
- }
-
- private class BleDeviceDisappearedScheduler extends Handler {
- BleDeviceDisappearedScheduler() {
- super(Looper.getMainLooper());
- }
-
- void scheduleBleDeviceDisappeared(int associationId) {
- if (hasMessages(associationId)) {
- removeMessages(associationId);
- }
- Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " ).");
- sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */);
- }
-
- void unScheduleDeviceDisappeared(int associationId) {
- if (hasMessages(associationId)) {
- Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )");
- synchronized (mBtDisconnectedDevices) {
- mBtDisconnectedDevices.remove(associationId);
- mBtDisconnectedDevicesBlePresence.delete(associationId);
- }
-
- removeMessages(associationId);
- }
- }
-
- @Override
- public void handleMessage(@NonNull Message msg) {
- final int associationId = msg.what;
- synchronized (mBtDisconnectedDevices) {
- final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(
- associationId);
- // If a device hasn't reported after 10 seconds and is not currently present,
- // assume BLE is lost and trigger the onDeviceEvent callback with the
- // EVENT_BLE_DISAPPEARED event.
- if (mBtDisconnectedDevices.contains(associationId)
- && !isCurrentPresent) {
- Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, "
- + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )");
- onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
- }
-
- mBtDisconnectedDevices.remove(associationId);
- mBtDisconnectedDevicesBlePresence.delete(associationId);
- }
- }
- }
-}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
similarity index 83%
rename from services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
rename to services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
index 5abdb42..c01c319 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.companion;
+package com.android.server.companion.presence;
import static android.content.Context.BIND_ALMOST_PERCEPTIBLE;
import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE;
@@ -33,36 +33,42 @@
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
-import android.util.Log;
+import android.util.Slog;
import com.android.internal.infra.ServiceConnector;
import com.android.server.ServiceThread;
+import com.android.server.companion.CompanionDeviceManagerService;
/**
* Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the
* application process.
*/
@SuppressLint("LongLogTag")
-class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> {
+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);
+ }
+
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;
-
- /** 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;
+ @UserIdInt
+ private final int mUserId;
+ @NonNull
+ private final ComponentName mComponentName;
+ private final boolean mIsPrimary;
// IMPORTANT: this can (and will!) be null (at the moment, CompanionApplicationController only
// installs a listener to the primary ServiceConnector), hence we should always null-check the
// reference before calling on it.
- private @Nullable Listener mListener;
- private boolean mIsPrimary;
+ @Nullable
+ private Listener mListener;
/**
* Create a CompanionDeviceServiceConnector instance.
@@ -79,16 +85,16 @@
* IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the
* service importance level should be higher than 125.
*/
- static CompanionDeviceServiceConnector newInstance(@NonNull Context context,
+ static CompanionServiceConnector newInstance(@NonNull Context context,
@UserIdInt int userId, @NonNull ComponentName componentName, boolean isSelfManaged,
boolean isPrimary) {
final int bindingFlags = isSelfManaged ? BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
: BIND_ALMOST_PERCEPTIBLE;
- return new CompanionDeviceServiceConnector(
+ return new CompanionServiceConnector(
context, userId, componentName, bindingFlags, isPrimary);
}
- private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId,
+ private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId,
@NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) {
super(context, buildIntent(componentName), bindingFlags, userId, null);
mUserId = userId;
@@ -133,6 +139,7 @@
return mIsPrimary;
}
+ @NonNull
ComponentName getComponentName() {
return mComponentName;
}
@@ -140,17 +147,15 @@
@Override
protected void onServiceConnectionStatusChanged(
@NonNull ICompanionDeviceService service, boolean isConnected) {
- if (DEBUG) {
- Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString()
- + " connected=" + isConnected);
- }
+ Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString()
+ + " connected=" + isConnected);
}
@Override
public void binderDied() {
super.binderDied();
- if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString());
+ Slog.d(TAG, "binderDied() " + mComponentName.toShortString());
// Handle primary process being killed
if (mListener != null) {
@@ -172,7 +177,8 @@
* within system_server and thus tends to get heavily congested)
*/
@Override
- protected @NonNull Handler getJobHandler() {
+ @NonNull
+ protected Handler getJobHandler() {
return getServiceThread().getThreadHandler();
}
@@ -182,12 +188,14 @@
return -1;
}
- private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) {
+ @NonNull
+ private static Intent buildIntent(@NonNull ComponentName componentName) {
return new Intent(CompanionDeviceService.SERVICE_INTERFACE)
.setComponent(componentName);
}
- private static @NonNull ServiceThread getServiceThread() {
+ @NonNull
+ private static ServiceThread getServiceThread() {
if (sServiceThread == null) {
synchronized (CompanionDeviceManagerService.class) {
if (sServiceThread == null) {
@@ -206,5 +214,6 @@
* <p>
* Do NOT reference directly, use {@link #getServiceThread()} method instead.
*/
- private static volatile @Nullable ServiceThread sServiceThread;
+ @Nullable
+ private static volatile ServiceThread sServiceThread;
}
diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
new file mode 100644
index 0000000..642460e
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
@@ -0,0 +1,1042 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.presence;
+
+import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.NO_ASSOCIATION;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SHELL_UID;
+
+import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObserveDevicePresenceByUuid;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.annotation.UserIdInt;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.companion.AssociationInfo;
+import android.companion.DeviceNotAssociatedException;
+import android.companion.DevicePresenceEvent;
+import android.companion.ObservingDevicePresenceRequest;
+import android.content.Context;
+import android.hardware.power.Mode;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.os.PowerManagerInternal;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.companion.association.AssociationStore;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class responsible for monitoring companion devices' "presence" status (i.e.
+ * connected/disconnected for Bluetooth devices; nearby or not for BLE devices).
+ *
+ * <p>
+ * Should only be used by
+ * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
+ * to which it provides the following API:
+ * <ul>
+ * <li> {@link #onSelfManagedDeviceConnected(int)}
+ * <li> {@link #onSelfManagedDeviceDisconnected(int)}
+ * <li> {@link #isDevicePresent(int)}
+ * </ul>
+ */
+@SuppressLint("LongLogTag")
+public class DevicePresenceProcessor implements AssociationStore.OnChangeListener,
+ BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback {
+ static final boolean DEBUG = false;
+ private static final String TAG = "CDM_DevicePresenceProcessor";
+
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final CompanionAppBinder mCompanionAppBinder;
+ @NonNull
+ private final AssociationStore mAssociationStore;
+ @NonNull
+ private final ObservableUuidStore mObservableUuidStore;
+ @NonNull
+ private final BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
+ @NonNull
+ private final BleCompanionDeviceScanner mBleScanner;
+ @NonNull
+ private final PowerManagerInternal mPowerManagerInternal;
+
+ // NOTE: Same association may appear in more than one of the following sets at the same time.
+ // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
+ // companion applications, while at the same be connected via BT, or detected nearby by BLE
+ // scanner)
+ @NonNull
+ private final Set<Integer> mConnectedBtDevices = new HashSet<>();
+ @NonNull
+ private final Set<Integer> mNearbyBleDevices = new HashSet<>();
+ @NonNull
+ private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+ @NonNull
+ private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
+ @NonNull
+ @GuardedBy("mBtDisconnectedDevices")
+ private final Set<Integer> mBtDisconnectedDevices = new HashSet<>();
+
+ // A map to track device presence within 10 seconds of Bluetooth disconnection.
+ // The key is the association ID, and the boolean value indicates if the device
+ // was detected again within that time frame.
+ @GuardedBy("mBtDisconnectedDevices")
+ private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence =
+ new SparseBooleanArray();
+
+ // Tracking "simulated" presence. Used for debugging and testing only.
+ private final @NonNull Set<Integer> mSimulated = new HashSet<>();
+ private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper =
+ new SimulatedDevicePresenceSchedulerHelper();
+
+ private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
+ new BleDeviceDisappearedScheduler();
+
+ public DevicePresenceProcessor(@NonNull Context context,
+ @NonNull CompanionAppBinder companionAppBinder,
+ UserManager userManager,
+ @NonNull AssociationStore associationStore,
+ @NonNull ObservableUuidStore observableUuidStore,
+ @NonNull PowerManagerInternal powerManagerInternal) {
+ mContext = context;
+ mCompanionAppBinder = companionAppBinder;
+ mAssociationStore = associationStore;
+ mObservableUuidStore = observableUuidStore;
+ mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
+ associationStore, mObservableUuidStore,
+ /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
+ mBleScanner = new BleCompanionDeviceScanner(associationStore,
+ /* BleCompanionDeviceScanner.Callback */ this);
+ mPowerManagerInternal = powerManagerInternal;
+ }
+
+ /** Initialize {@link DevicePresenceProcessor} */
+ public void init(Context context) {
+ if (DEBUG) Slog.i(TAG, "init()");
+
+ final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (btAdapter != null) {
+ mBtConnectionListener.init(btAdapter);
+ mBleScanner.init(context, btAdapter);
+ } else {
+ Slog.w(TAG, "BluetoothAdapter is NOT available.");
+ }
+
+ mAssociationStore.registerLocalListener(this);
+ }
+
+ /**
+ * Process device presence start request.
+ */
+ public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
+ String packageName, int userId) {
+ Slog.i(TAG,
+ "Start observing request=[" + request + "] for userId=[" + userId + "], package=["
+ + packageName + "]...");
+ final ParcelUuid requestUuid = request.getUuid();
+
+ if (requestUuid != null) {
+ enforceCallerCanObserveDevicePresenceByUuid(mContext);
+
+ // If it's already being observed, then no-op.
+ if (mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) {
+ Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=["
+ + userId + "] is already being observed.");
+ return;
+ }
+
+ final ObservableUuid observableUuid = new ObservableUuid(userId, requestUuid,
+ packageName, System.currentTimeMillis());
+ mObservableUuidStore.writeObservableUuid(userId, observableUuid);
+ } else {
+ final int associationId = request.getAssociationId();
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
+
+ // If it's already being observed, then no-op.
+ if (association.isNotifyOnDeviceNearby()) {
+ Slog.i(TAG, "Associated device id=[" + association.getId()
+ + "] is already being observed. No-op.");
+ return;
+ }
+
+ association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(true)
+ .build();
+ mAssociationStore.updateAssociation(association);
+
+ // Send callback immediately if the device is present.
+ if (isDevicePresent(associationId)) {
+ Slog.i(TAG, "Device is already present. Triggering callback.");
+ if (isBlePresent(associationId)) {
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
+ } else if (isBtConnected(associationId)) {
+ onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
+ } else if (isSimulatePresent(associationId)) {
+ onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_APPEARED);
+ }
+ }
+ }
+
+ Slog.i(TAG, "Registered device presence listener.");
+ }
+
+ /**
+ * Process device presence stop request.
+ */
+ public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
+ String packageName, int userId) {
+ Slog.i(TAG,
+ "Stop observing request=[" + request + "] for userId=[" + userId + "], package=["
+ + packageName + "]...");
+
+ final ParcelUuid requestUuid = request.getUuid();
+
+ if (requestUuid != null) {
+ enforceCallerCanObserveDevicePresenceByUuid(mContext);
+
+ if (!mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) {
+ Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=["
+ + userId + "] is already not being observed.");
+ return;
+ }
+
+ mObservableUuidStore.removeObservableUuid(userId, requestUuid, packageName);
+ removeCurrentConnectedUuidDevice(requestUuid);
+ } else {
+ final int associationId = request.getAssociationId();
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
+
+ // If it's already being observed, then no-op.
+ if (!association.isNotifyOnDeviceNearby()) {
+ Slog.i(TAG, "Associated device id=[" + association.getId()
+ + "] is already not being observed. No-op.");
+ return;
+ }
+
+ association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(false)
+ .build();
+ mAssociationStore.updateAssociation(association);
+ }
+
+ Slog.i(TAG, "Unregistered device presence listener.");
+
+ // If last listener is unregistered, then unbind application.
+ if (!shouldBindPackage(userId, packageName)) {
+ mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+ }
+ }
+
+ /**
+ * For legacy device presence below Android V.
+ *
+ * @deprecated Use {@link #startObservingDevicePresence(ObservingDevicePresenceRequest, String,
+ * int)}
+ */
+ @Deprecated
+ public void startObservingDevicePresence(int userId, String packageName, String deviceAddress)
+ throws RemoteException {
+ Slog.i(TAG,
+ "Start observing device=[" + deviceAddress + "] for userId=[" + userId
+ + "], package=["
+ + packageName + "]...");
+
+ enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null);
+
+ AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+ packageName, deviceAddress);
+
+ if (association == null) {
+ throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+ + " is not associated with device " + deviceAddress
+ + " for user " + userId));
+ }
+
+ startObservingDevicePresence(
+ new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId())
+ .build(), packageName, userId);
+ }
+
+ /**
+ * For legacy device presence below Android V.
+ *
+ * @deprecated Use {@link #stopObservingDevicePresence(ObservingDevicePresenceRequest, String,
+ * int)}
+ */
+ @Deprecated
+ public void stopObservingDevicePresence(int userId, String packageName, String deviceAddress)
+ throws RemoteException {
+ Slog.i(TAG,
+ "Stop observing device=[" + deviceAddress + "] for userId=[" + userId
+ + "], package=["
+ + packageName + "]...");
+
+ enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null);
+
+ AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+ packageName, deviceAddress);
+
+ if (association == null) {
+ throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+ + " is not associated with device " + deviceAddress
+ + " for user " + userId));
+ }
+
+ stopObservingDevicePresence(
+ new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId())
+ .build(), packageName, userId);
+ }
+
+ /**
+ * @return whether the package should be bound (i.e. at least one of the devices associated with
+ * the package is currently present OR the UUID to be observed by this package is
+ * currently present).
+ */
+ private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
+ final List<AssociationInfo> packageAssociations =
+ mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+ for (AssociationInfo association : packageAssociations) {
+ if (!association.shouldBindWhenPresent()) continue;
+ if (isDevicePresent(association.getId())) return true;
+ }
+
+ for (ObservableUuid uuid : observableUuids) {
+ if (isDeviceUuidPresent(uuid.getUuid())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Bind the system to the app if it's not bound.
+ *
+ * Set bindImportant to true when the association is self-managed to avoid the target service
+ * being killed.
+ */
+ private void bindApplicationIfNeeded(int userId, String packageName, boolean bindImportant) {
+ if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+ mCompanionAppBinder.bindCompanionApp(
+ userId, packageName, bindImportant, this::onBinderDied);
+ } else {
+ Slog.i(TAG,
+ "UserId=[" + userId + "], packageName=[" + packageName + "] is already bound.");
+ }
+ }
+
+ /**
+ * @return current connected UUID devices.
+ */
+ public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
+ return mConnectedUuidDevices;
+ }
+
+ /**
+ * Remove current connected UUID device.
+ */
+ public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
+ mConnectedUuidDevices.remove(uuid);
+ }
+
+ /**
+ * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
+ * or devices is connected (for Bluetooth); or reported (by the application) to be
+ * nearby (for "self-managed" associations).
+ */
+ public boolean isDevicePresent(int associationId) {
+ return mReportedSelfManagedDevices.contains(associationId)
+ || mConnectedBtDevices.contains(associationId)
+ || mNearbyBleDevices.contains(associationId)
+ || mSimulated.contains(associationId);
+ }
+
+ /**
+ * @return whether the current uuid to be observed is present.
+ */
+ public boolean isDeviceUuidPresent(ParcelUuid uuid) {
+ return mConnectedUuidDevices.contains(uuid);
+ }
+
+ /**
+ * @return whether the current device is BT connected and had already reported to the app.
+ */
+
+ public boolean isBtConnected(int associationId) {
+ return mConnectedBtDevices.contains(associationId);
+ }
+
+ /**
+ * @return whether the current device in BLE range and had already reported to the app.
+ */
+ public boolean isBlePresent(int associationId) {
+ return mNearbyBleDevices.contains(associationId);
+ }
+
+ /**
+ * @return whether the current device had been already reported by the simulator.
+ */
+ public boolean isSimulatePresent(int associationId) {
+ return mSimulated.contains(associationId);
+ }
+
+ /**
+ * Marks a "self-managed" device as connected.
+ *
+ * <p>
+ * Must ONLY be invoked by the
+ * {@link com.android.server.companion.CompanionDeviceManagerService
+ * CompanionDeviceManagerService}
+ * when an application invokes
+ * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int)
+ * notifyDeviceAppeared()}
+ */
+ public void onSelfManagedDeviceConnected(int associationId) {
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_APPEARED);
+ }
+
+ /**
+ * Marks a "self-managed" device as disconnected.
+ *
+ * <p>
+ * Must ONLY be invoked by the
+ * {@link com.android.server.companion.CompanionDeviceManagerService
+ * CompanionDeviceManagerService}
+ * when an application invokes
+ * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int)
+ * notifyDeviceDisappeared()}
+ */
+ public void onSelfManagedDeviceDisconnected(int associationId) {
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_DISAPPEARED);
+ }
+
+ /**
+ * Marks a "self-managed" device as disconnected when binderDied.
+ */
+ public void onSelfManagedDeviceReporterBinderDied(int associationId) {
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_DISAPPEARED);
+ }
+
+ @Override
+ public void onBluetoothCompanionDeviceConnected(int associationId) {
+ synchronized (mBtDisconnectedDevices) {
+ // A device is considered reconnected within 10 seconds if a pending BLE lost report is
+ // followed by a detected Bluetooth connection.
+ boolean isReconnected = mBtDisconnectedDevices.contains(associationId);
+ if (isReconnected) {
+ Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s.");
+ mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
+ }
+
+ Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
+ + "associationId( " + associationId + " )");
+ onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
+
+ // Stop the BLE scan if all devices report BT connected status and BLE was present.
+ if (canStopBleScan()) {
+ mBleScanner.stopScanIfNeeded();
+ }
+
+ }
+ }
+
+ @Override
+ public void onBluetoothCompanionDeviceDisconnected(int associationId) {
+ Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
+ + "associationId( " + associationId + " )");
+ // Start BLE scanning when the device is disconnected.
+ mBleScanner.startScan();
+
+ onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
+ // If current device is BLE present but BT is disconnected , means it will be
+ // potentially out of range later. Schedule BLE disappeared callback.
+ if (isBlePresent(associationId)) {
+ synchronized (mBtDisconnectedDevices) {
+ mBtDisconnectedDevices.add(associationId);
+ }
+ mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId);
+ }
+ }
+
+
+ @Override
+ public void onBleCompanionDeviceFound(int associationId) {
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
+ synchronized (mBtDisconnectedDevices) {
+ final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
+ if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) {
+ mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
+ }
+ }
+ }
+
+ @Override
+ public void onBleCompanionDeviceLost(int associationId) {
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
+ }
+
+ /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+ @TestApi
+ public void simulateDeviceEvent(int associationId, int event) {
+ // IMPORTANT: this API should only be invoked via the
+ // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
+ // make this call are SHELL and ROOT.
+ // No other caller (including SYSTEM!) should be allowed.
+ enforceCallerShellOrRoot();
+ // Make sure the association exists.
+ enforceAssociationExists(associationId);
+
+ switch (event) {
+ case EVENT_BLE_APPEARED:
+ simulateDeviceAppeared(associationId, event);
+ break;
+ case EVENT_BT_CONNECTED:
+ onBluetoothCompanionDeviceConnected(associationId);
+ break;
+ case EVENT_BLE_DISAPPEARED:
+ simulateDeviceDisappeared(associationId, event);
+ break;
+ case EVENT_BT_DISCONNECTED:
+ onBluetoothCompanionDeviceDisconnected(associationId);
+ break;
+ default:
+ throw new IllegalArgumentException("Event: " + event + "is not supported");
+ }
+ }
+
+ /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+ @TestApi
+ public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
+ // IMPORTANT: this API should only be invoked via the
+ // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to
+ // make this call are SHELL and ROOT.
+ // No other caller (including SYSTEM!) should be allowed.
+ enforceCallerShellOrRoot();
+ onDevicePresenceEventByUuid(uuid, event);
+ }
+
+ private void simulateDeviceAppeared(int associationId, int state) {
+ onDevicePresenceEvent(mSimulated, associationId, state);
+ mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
+ }
+
+ private void simulateDeviceDisappeared(int associationId, int state) {
+ mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
+ onDevicePresenceEvent(mSimulated, associationId, state);
+ }
+
+ private void enforceAssociationExists(int associationId) {
+ if (mAssociationStore.getAssociationById(associationId) == null) {
+ throw new IllegalArgumentException(
+ "Association with id " + associationId + " does not exist.");
+ }
+ }
+
+ private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
+ int associationId, int eventType) {
+ Slog.i(TAG,
+ "onDevicePresenceEvent() id=[" + associationId + "], event=[" + eventType + "]...");
+
+ AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+ if (association == null) {
+ Slog.e(TAG, "Association doesn't exist.");
+ return;
+ }
+
+ final int userId = association.getUserId();
+ final String packageName = association.getPackageName();
+ final DevicePresenceEvent event = new DevicePresenceEvent(associationId, eventType, null);
+
+ if (eventType == EVENT_BLE_APPEARED) {
+ synchronized (mBtDisconnectedDevices) {
+ // If a BLE device is detected within 10 seconds after BT is disconnected,
+ // flag it as BLE is present.
+ if (mBtDisconnectedDevices.contains(associationId)) {
+ Slog.i(TAG, "Device ( " + associationId + " ) is present,"
+ + " do not need to send the callback with event ( "
+ + EVENT_BLE_APPEARED + " ).");
+ mBtDisconnectedDevicesBlePresence.append(associationId, true);
+ }
+ }
+ }
+
+ switch (eventType) {
+ case EVENT_BLE_APPEARED:
+ case EVENT_BT_CONNECTED:
+ case EVENT_SELF_MANAGED_APPEARED:
+ final boolean added = presentDevicesForSource.add(associationId);
+ if (!added) {
+ Slog.w(TAG, "The association is already present.");
+ }
+
+ if (association.shouldBindWhenPresent()) {
+ bindApplicationIfNeeded(userId, packageName, association.isSelfManaged());
+ } else {
+ return;
+ }
+
+ if (association.isSelfManaged() || added) {
+ notifyDevicePresenceEvent(userId, packageName, event);
+ // Also send the legacy callback.
+ legacyNotifyDevicePresenceEvent(association, true);
+ }
+ break;
+ case EVENT_BLE_DISAPPEARED:
+ case EVENT_BT_DISCONNECTED:
+ case EVENT_SELF_MANAGED_DISAPPEARED:
+ final boolean removed = presentDevicesForSource.remove(associationId);
+ if (!removed) {
+ Slog.w(TAG, "The association is already NOT present.");
+ }
+
+ if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+ Slog.e(TAG, "Package is not bound");
+ return;
+ }
+
+ if (association.isSelfManaged() || removed) {
+ notifyDevicePresenceEvent(userId, packageName, event);
+ // Also send the legacy callback.
+ legacyNotifyDevicePresenceEvent(association, false);
+ }
+
+ // Check if there are other devices associated to the app that are present.
+ if (!shouldBindPackage(userId, packageName)) {
+ mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+ }
+ break;
+ default:
+ Slog.e(TAG, "Event: " + eventType + " is not supported.");
+ break;
+ }
+ }
+
+ @Override
+ public void onDevicePresenceEventByUuid(ObservableUuid uuid, int eventType) {
+ Slog.i(TAG, "onDevicePresenceEventByUuid ObservableUuid=[" + uuid + "], event=[" + eventType
+ + "]...");
+
+ final ParcelUuid parcelUuid = uuid.getUuid();
+ final String packageName = uuid.getPackageName();
+ final int userId = uuid.getUserId();
+ final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType,
+ parcelUuid);
+
+ switch (eventType) {
+ case EVENT_BT_CONNECTED:
+ boolean added = mConnectedUuidDevices.add(parcelUuid);
+ if (!added) {
+ Slog.w(TAG, "This device is already connected.");
+ }
+
+ bindApplicationIfNeeded(userId, packageName, false);
+
+ notifyDevicePresenceEvent(userId, packageName, event);
+ break;
+ case EVENT_BT_DISCONNECTED:
+ final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
+ if (!removed) {
+ Slog.w(TAG, "This device is already disconnected.");
+ return;
+ }
+
+ if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+ Slog.e(TAG, "Package is not bound.");
+ return;
+ }
+
+ notifyDevicePresenceEvent(userId, packageName, event);
+
+ if (!shouldBindPackage(userId, packageName)) {
+ mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+ }
+ break;
+ default:
+ Slog.e(TAG, "Event: " + eventType + " is not supported");
+ break;
+ }
+ }
+
+ /**
+ * Notify device presence event to the app.
+ *
+ * @deprecated Use {@link #notifyDevicePresenceEvent(int, String, DevicePresenceEvent)} instead.
+ */
+ @Deprecated
+ private void legacyNotifyDevicePresenceEvent(AssociationInfo association,
+ boolean isAppeared) {
+ Slog.i(TAG, "legacyNotifyDevicePresenceEvent() association=[" + association.toShortString()
+ + "], isAppeared=[" + isAppeared + "]");
+
+ final int userId = association.getUserId();
+ final String packageName = association.getPackageName();
+
+ final CompanionServiceConnector primaryServiceConnector =
+ mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName);
+ if (primaryServiceConnector == null) {
+ Slog.e(TAG, "Package is not bound.");
+ return;
+ }
+
+ if (isAppeared) {
+ primaryServiceConnector.postOnDeviceAppeared(association);
+ } else {
+ primaryServiceConnector.postOnDeviceDisappeared(association);
+ }
+ }
+
+ /**
+ * Notify the device presence event to the app.
+ */
+ private void notifyDevicePresenceEvent(int userId, String packageName,
+ DevicePresenceEvent event) {
+ Slog.i(TAG,
+ "notifyCompanionDevicePresenceEvent userId=[" + userId + "], packageName=["
+ + packageName + "], event=[" + event + "]...");
+
+ final CompanionServiceConnector primaryServiceConnector =
+ mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName);
+
+ if (primaryServiceConnector == null) {
+ Slog.e(TAG, "Package is NOT bound.");
+ return;
+ }
+
+ primaryServiceConnector.postOnDevicePresenceEvent(event);
+ }
+
+ /**
+ * Notify the self-managed device presence event to the app.
+ */
+ public void notifySelfManagedDevicePresenceEvent(int associationId, boolean isAppeared) {
+ Slog.i(TAG, "notifySelfManagedDeviceAppeared() id=" + associationId);
+
+ AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+ associationId);
+ if (!association.isSelfManaged()) {
+ throw new IllegalArgumentException("Association id=[" + associationId
+ + "] is not self-managed.");
+ }
+ // AssociationInfo class is immutable: create a new AssociationInfo object with updated
+ // timestamp.
+ association = (new AssociationInfo.Builder(association))
+ .setLastTimeConnected(System.currentTimeMillis())
+ .build();
+ mAssociationStore.updateAssociation(association);
+
+ if (isAppeared) {
+ onSelfManagedDeviceConnected(associationId);
+ } else {
+ onSelfManagedDeviceDisconnected(associationId);
+ }
+
+ final String deviceProfile = association.getDeviceProfile();
+ if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+ Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile);
+ mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, isAppeared);
+ }
+ }
+
+ private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
+ @NonNull CompanionServiceConnector serviceConnector) {
+
+ boolean isPrimary = serviceConnector.isPrimary();
+ Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
+
+ // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY.
+ if (isPrimary) {
+ final List<AssociationInfo> associations =
+ mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
+
+ for (AssociationInfo association : associations) {
+ final String deviceProfile = association.getDeviceProfile();
+ if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+ Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
+ mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
+ break;
+ }
+ }
+
+ mCompanionAppBinder.removePackage(userId, packageName);
+ }
+
+ // Second: schedule rebinding if needed.
+ final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
+
+ if (shouldScheduleRebind) {
+ mCompanionAppBinder.scheduleRebinding(userId, packageName, serviceConnector);
+ }
+ }
+
+ /**
+ * Check if the system should rebind the self-managed secondary services
+ * OR non-self-managed services.
+ */
+ private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
+ // Make sure do not schedule rebind for the case ServiceConnector still gets callback after
+ // app is uninstalled.
+ boolean stillAssociated = false;
+ // Make sure to clean up the state for all the associations
+ // that associate with this package.
+ boolean shouldScheduleRebind = false;
+ boolean shouldScheduleRebindForUuid = false;
+ final List<ObservableUuid> uuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+ for (AssociationInfo ai :
+ mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) {
+ final int associationId = ai.getId();
+ stillAssociated = true;
+ if (ai.isSelfManaged()) {
+ // Do not rebind if primary one is died for selfManaged application.
+ if (isPrimary && isDevicePresent(associationId)) {
+ onSelfManagedDeviceReporterBinderDied(associationId);
+ shouldScheduleRebind = false;
+ }
+ // Do not rebind if both primary and secondary services are died for
+ // selfManaged application.
+ shouldScheduleRebind = mCompanionAppBinder.isCompanionApplicationBound(userId,
+ packageName);
+ } else if (ai.isNotifyOnDeviceNearby()) {
+ // Always rebind for non-selfManaged devices.
+ shouldScheduleRebind = true;
+ }
+ }
+
+ for (ObservableUuid uuid : uuids) {
+ if (isDeviceUuidPresent(uuid.getUuid())) {
+ shouldScheduleRebindForUuid = true;
+ break;
+ }
+ }
+
+ return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
+ }
+
+ /**
+ * Implements
+ * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
+ */
+ @Override
+ public void onAssociationRemoved(@NonNull AssociationInfo association) {
+ final int id = association.getId();
+ if (DEBUG) {
+ Log.i(TAG, "onAssociationRemoved() id=" + id);
+ Log.d(TAG, " > association=" + association);
+ }
+
+ mConnectedBtDevices.remove(id);
+ mNearbyBleDevices.remove(id);
+ mReportedSelfManagedDevices.remove(id);
+ mSimulated.remove(id);
+ synchronized (mBtDisconnectedDevices) {
+ mBtDisconnectedDevices.remove(id);
+ mBtDisconnectedDevicesBlePresence.delete(id);
+ }
+
+ // Do NOT call mCallback.onDeviceDisappeared()!
+ // CompanionDeviceManagerService will know that the association is removed, and will do
+ // what's needed.
+ }
+
+ /**
+ * Return a set of devices that pending to report connectivity
+ */
+ public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
+ synchronized (mBtConnectionListener.mPendingConnectedDevices) {
+ return mBtConnectionListener.mPendingConnectedDevices;
+ }
+ }
+
+ private static void enforceCallerShellOrRoot() {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid == SHELL_UID || callingUid == ROOT_UID) return;
+
+ throw new SecurityException("Caller is neither Shell nor Root");
+ }
+
+ /**
+ * The BLE scan can be only stopped if all the devices have been reported
+ * BT connected and BLE presence and are not pending to report BLE lost.
+ */
+ private boolean canStopBleScan() {
+ for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) {
+ int id = ai.getId();
+ synchronized (mBtDisconnectedDevices) {
+ if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id)
+ && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) {
+ Slog.i(TAG, "The BLE scan cannot be stopped, "
+ + "device( " + id + " ) is not yet connected "
+ + "OR the BLE is not current present Or is pending to report BLE lost");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Dumps system information about devices that are marked as "present".
+ */
+ public void dump(@NonNull PrintWriter out) {
+ out.append("Companion Device Present: ");
+ if (mConnectedBtDevices.isEmpty()
+ && mNearbyBleDevices.isEmpty()
+ && mReportedSelfManagedDevices.isEmpty()) {
+ out.append("<empty>\n");
+ return;
+ } else {
+ out.append("\n");
+ }
+
+ out.append(" Connected Bluetooth Devices: ");
+ if (mConnectedBtDevices.isEmpty()) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (int associationId : mConnectedBtDevices) {
+ AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+ out.append(" ").append(a.toShortString()).append('\n');
+ }
+ }
+
+ out.append(" Nearby BLE Devices: ");
+ if (mNearbyBleDevices.isEmpty()) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (int associationId : mNearbyBleDevices) {
+ AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+ out.append(" ").append(a.toShortString()).append('\n');
+ }
+ }
+
+ out.append(" Self-Reported Devices: ");
+ if (mReportedSelfManagedDevices.isEmpty()) {
+ out.append("<empty>\n");
+ } else {
+ out.append("\n");
+ for (int associationId : mReportedSelfManagedDevices) {
+ AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+ out.append(" ").append(a.toShortString()).append('\n');
+ }
+ }
+ }
+
+ private class SimulatedDevicePresenceSchedulerHelper extends Handler {
+ SimulatedDevicePresenceSchedulerHelper() {
+ super(Looper.getMainLooper());
+ }
+
+ void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
+ // First, unschedule if it was scheduled previously.
+ if (hasMessages(/* what */ associationId)) {
+ removeMessages(/* what */ associationId);
+ }
+
+ sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */);
+ }
+
+ void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
+ removeMessages(/* what */ associationId);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ final int associationId = msg.what;
+ if (mSimulated.contains(associationId)) {
+ onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
+ }
+ }
+ }
+
+ private class BleDeviceDisappearedScheduler extends Handler {
+ BleDeviceDisappearedScheduler() {
+ super(Looper.getMainLooper());
+ }
+
+ void scheduleBleDeviceDisappeared(int associationId) {
+ if (hasMessages(associationId)) {
+ removeMessages(associationId);
+ }
+ Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " ).");
+ sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */);
+ }
+
+ void unScheduleDeviceDisappeared(int associationId) {
+ if (hasMessages(associationId)) {
+ Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )");
+ synchronized (mBtDisconnectedDevices) {
+ mBtDisconnectedDevices.remove(associationId);
+ mBtDisconnectedDevicesBlePresence.delete(associationId);
+ }
+
+ removeMessages(associationId);
+ }
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ final int associationId = msg.what;
+ synchronized (mBtDisconnectedDevices) {
+ final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(
+ associationId);
+ // If a device hasn't reported after 10 seconds and is not currently present,
+ // assume BLE is lost and trigger the onDeviceEvent callback with the
+ // EVENT_BLE_DISAPPEARED event.
+ if (mBtDisconnectedDevices.contains(associationId)
+ && !isCurrentPresent) {
+ Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, "
+ + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )");
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
+ }
+
+ mBtDisconnectedDevices.remove(associationId);
+ mBtDisconnectedDevicesBlePresence.delete(associationId);
+ }
+ }
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
index db15da29..fa0f6bd 100644
--- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
+++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
@@ -300,4 +300,18 @@
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 793fb7f..697ef87 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -46,7 +46,6 @@
@SuppressLint("LongLogTag")
public class CompanionTransportManager {
private static final String TAG = "CDM_CompanionTransportManager";
- private static final boolean DEBUG = false;
private boolean mSecureTransportEnabled = true;
@@ -137,11 +136,17 @@
}
}
- public void attachSystemDataTransport(String packageName, int userId, int associationId,
- ParcelFileDescriptor fd) {
+ /**
+ * Attach transport.
+ */
+ public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) {
+ Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]...");
+
+ mAssociationStore.getAssociationWithCallerChecks(associationId);
+
synchronized (mTransports) {
if (mTransports.contains(associationId)) {
- detachSystemDataTransport(packageName, userId, associationId);
+ detachSystemDataTransport(associationId);
}
// TODO: Implement new API to pass a PSK
@@ -149,9 +154,18 @@
notifyOnTransportsChanged();
}
+
+ Slog.i(TAG, "Transport attached.");
}
- public void detachSystemDataTransport(String packageName, int userId, int associationId) {
+ /**
+ * Detach transport.
+ */
+ public void detachSystemDataTransport(int associationId) {
+ Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]...");
+
+ mAssociationStore.getAssociationWithCallerChecks(associationId);
+
synchronized (mTransports) {
final Transport transport = mTransports.removeReturnOld(associationId);
if (transport == null) {
@@ -161,6 +175,8 @@
transport.stop();
notifyOnTransportsChanged();
}
+
+ Slog.i(TAG, "Transport detached.");
}
private void notifyOnTransportsChanged() {
@@ -307,8 +323,7 @@
int associationId = transport.mAssociationId;
AssociationInfo association = mAssociationStore.getAssociationById(associationId);
if (association != null) {
- detachSystemDataTransport(association.getPackageName(),
- association.getUserId(),
+ detachSystemDataTransport(
association.getId());
}
}
diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
index 2cf1f46..d7e766e 100644
--- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
@@ -39,7 +39,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
-import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
import android.companion.CompanionDeviceManager;
import android.content.Context;
@@ -208,7 +207,7 @@
/**
* Require the caller to hold necessary permission to observe device presence by UUID.
*/
- public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) {
+ public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) {
if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
!= PERMISSION_GRANTED) {
throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
@@ -235,23 +234,6 @@
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/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/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/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
index 0b16af2..dbcd21a 100644
--- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java
+++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java
@@ -46,29 +46,44 @@
* @param desired The locale preferred by user.
* @return A score based on the locale matching for the default subtype enabling.
*/
- @IntRange(from = 1, to = 3)
+ @IntRange(from = 1, to = 4)
private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
@NonNull final ULocale desired) {
// Assuming supported/desired is fully expanded.
if (supported.equals(desired)) {
- return 3; // Exact match.
+ return 4; // Exact match.
}
+ // addLikelySubtags is a maximization process as per
+ // https://www.unicode.org/reports/tr35/#Likely_Subtags
+ ULocale maxDesired = ULocale.addLikelySubtags(desired);
+
// Skip language matching since it was already done in calculateMatchingScore.
final String supportedScript = supported.getScript();
- if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) {
+ if (supportedScript.isEmpty() || !supportedScript.equals(maxDesired.getScript())) {
// TODO: Need subscript matching. For example, Hanb should match with Bopo.
return 1;
}
final String supportedCountry = supported.getCountry();
- if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) {
+ if (supportedCountry.isEmpty() || !supportedCountry.equals(maxDesired.getCountry())) {
return 2;
}
// Ignore others e.g. variants, extensions.
- return 3;
+
+ // Since addLikelySubtags can canonicalize subtags, e.g. the deprecated country codes
+ // an locale with an identical script and country before addLikelySubtags is in favour,
+ // and a score of 4 is returned.
+ String desiredScript = desired.getScript();
+ String desiredCountry = desired.getCountry();
+ if ((desiredScript.isEmpty() || desiredScript.equals(maxDesired.getScript()))
+ && (desiredCountry.isEmpty() || desiredCountry.equals(maxDesired.getCountry()))) {
+ return 4;
+ } else {
+ return 3;
+ }
}
private static final class ScoreEntry implements Comparable<ScoreEntry> {
@@ -180,8 +195,7 @@
ULocale.forLocale(preferredLocale));
}
score[j] = calculateMatchingSubScore(
- preferredULocaleCache[j],
- ULocale.addLikelySubtags(ULocale.forLocale(locale)));
+ preferredULocaleCache[j], ULocale.forLocale(locale));
if (canSkip && score[j] != 0) {
canSkip = false;
}
diff --git a/services/core/java/com/android/server/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..eb4e6e4 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;
/**
@@ -429,28 +431,9 @@
int stream = getVolumeStream(mAudioAttrs);
final int volumeValue = value;
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- try {
- mAudioManager.setStreamVolumeForUid(
- stream,
- volumeValue,
- flags,
- opPackageName,
- uid,
- pid,
- mContext.getApplicationInfo().targetSdkVersion);
- } catch (IllegalArgumentException | SecurityException e) {
- Slog.e(
- TAG,
- "Cannot set volume: stream=" + stream
- + ", value=" + volumeValue
- + ", flags=" + flags,
- e);
- }
- }
- });
+ () ->
+ setStreamVolumeForUid(
+ opPackageName, pid, uid, flags, stream, volumeValue));
} else {
if (mVolumeControlType != VOLUME_CONTROL_ABSOLUTE) {
if (DEBUG) {
@@ -480,6 +463,27 @@
}
}
+ private void setStreamVolumeForUid(
+ String opPackageName, int pid, int uid, int flags, int stream, int volumeValue) {
+ try {
+ mAudioManager.setStreamVolumeForUid(
+ stream,
+ volumeValue,
+ flags,
+ opPackageName,
+ uid,
+ pid,
+ mContext.getApplicationInfo().targetSdkVersion);
+ } catch (IllegalArgumentException | SecurityException e) {
+ Slog.e(
+ TAG,
+ "Cannot set volume: stream=" + stream
+ + ", value=" + volumeValue
+ + ", flags=" + flags,
+ e);
+ }
+ }
+
/**
* Check if this session has been set to active by the app.
* <p>
@@ -639,6 +643,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;
@@ -738,52 +751,70 @@
pid = callingPid;
}
mHandler.post(
- new Runnable() {
- @Override
- public void run() {
- try {
- if (useSuggested) {
- if (AudioSystem.isStreamActive(stream, 0)) {
- mAudioManager.adjustSuggestedStreamVolumeForUid(
- stream,
- direction,
- flags,
- opPackageName,
- uid,
- pid,
- mContext.getApplicationInfo().targetSdkVersion);
- } else {
- mAudioManager.adjustSuggestedStreamVolumeForUid(
- AudioManager.USE_DEFAULT_STREAM_TYPE,
- direction,
- flags | previousFlagPlaySound,
- opPackageName,
- uid,
- pid,
- mContext.getApplicationInfo().targetSdkVersion);
- }
- } else {
- mAudioManager.adjustStreamVolumeForUid(
- stream,
- direction,
- flags,
- opPackageName,
- uid,
- pid,
- mContext.getApplicationInfo().targetSdkVersion);
- }
- } catch (IllegalArgumentException | SecurityException e) {
- Slog.e(
- TAG,
- "Cannot adjust volume: direction=" + direction
- + ", stream=" + stream + ", flags=" + flags
- + ", opPackageName=" + opPackageName + ", uid=" + uid
- + ", useSuggested=" + useSuggested
- + ", previousFlagPlaySound=" + previousFlagPlaySound,
- e);
- }
- }
- });
+ () ->
+ adjustSuggestedStreamVolumeForUid(
+ stream,
+ direction,
+ flags,
+ useSuggested,
+ previousFlagPlaySound,
+ opPackageName,
+ uid,
+ pid));
+ }
+
+ private void adjustSuggestedStreamVolumeForUid(
+ int stream,
+ int direction,
+ int flags,
+ boolean useSuggested,
+ int previousFlagPlaySound,
+ String opPackageName,
+ int uid,
+ int pid) {
+ try {
+ if (useSuggested) {
+ if (AudioSystem.isStreamActive(stream, 0)) {
+ mAudioManager.adjustSuggestedStreamVolumeForUid(
+ stream,
+ direction,
+ flags,
+ opPackageName,
+ uid,
+ pid,
+ mContext.getApplicationInfo().targetSdkVersion);
+ } else {
+ mAudioManager.adjustSuggestedStreamVolumeForUid(
+ AudioManager.USE_DEFAULT_STREAM_TYPE,
+ direction,
+ flags | previousFlagPlaySound,
+ opPackageName,
+ uid,
+ pid,
+ mContext.getApplicationInfo().targetSdkVersion);
+ }
+ } else {
+ mAudioManager.adjustStreamVolumeForUid(
+ stream,
+ direction,
+ flags,
+ opPackageName,
+ uid,
+ pid,
+ mContext.getApplicationInfo().targetSdkVersion);
+ }
+ } catch (IllegalArgumentException | SecurityException e) {
+ Slog.e(
+ TAG,
+ "Cannot adjust volume: direction=" + direction
+ + ", stream=" + stream
+ + ", flags=" + flags
+ + ", opPackageName=" + opPackageName
+ + ", uid=" + uid
+ + ", useSuggested=" + useSuggested
+ + ", previousFlagPlaySound=" + previousFlagPlaySound,
+ e);
+ }
}
private void logCallbackException(
@@ -1078,16 +1109,14 @@
volumeType, VOLUME_CONTROL_ABSOLUTE, max, current, attributes, null);
}
- private final Runnable mClearOptimisticVolumeRunnable = new Runnable() {
- @Override
- public void run() {
- boolean needUpdate = (mOptimisticVolume != mCurrentVolume);
- mOptimisticVolume = -1;
- if (needUpdate) {
- pushVolumeUpdate();
- }
- }
- };
+ private final Runnable mClearOptimisticVolumeRunnable =
+ () -> {
+ boolean needUpdate = (mOptimisticVolume != mCurrentVolume);
+ mOptimisticVolume = -1;
+ if (needUpdate) {
+ pushVolumeUpdate();
+ }
+ };
@RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
private static boolean componentNameExists(
diff --git a/services/core/java/com/android/server/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/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 3ecc58e..e1e2b3e 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -25,6 +25,8 @@
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
+import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
+
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -1433,7 +1435,7 @@
protected void rebindServices(boolean forceRebind, int userToRebind) {
if (DEBUG) Slog.d(TAG, "rebindServices " + forceRebind + " " + userToRebind);
IntArray userIds = mUserProfiles.getCurrentProfileIds();
- boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind)
+ boolean rebindAllCurrentUsers = mUserProfiles.isProfileUser(userToRebind, mContext)
&& allowRebindForParentUser();
if (userToRebind != USER_ALL && !rebindAllCurrentUsers) {
userIds = new IntArray(1);
@@ -1958,7 +1960,7 @@
* from receiving events from the profile.
*/
public boolean isPermittedForProfile(int userId) {
- if (!mUserProfiles.isProfileUser(userId)) {
+ if (!mUserProfiles.isProfileUser(userId, mContext)) {
return true;
}
DevicePolicyManager dpm =
@@ -2036,16 +2038,26 @@
}
}
- public boolean isProfileUser(int userId) {
+ public boolean isProfileUser(int userId, Context context) {
synchronized (mCurrentProfiles) {
UserInfo user = mCurrentProfiles.get(userId);
if (user == null) {
return false;
}
- if (user.isManagedProfile() || user.isCloneProfile()) {
- return true;
+ if (privateSpaceFlagsEnabled()) {
+ return user.isProfile() && hasParent(user, context);
}
- return false;
+ return user.isManagedProfile() || user.isCloneProfile();
+ }
+ }
+
+ boolean hasParent(UserInfo profile, Context context) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ UserManager um = context.getSystemService(UserManager.class);
+ return um.getProfileParent(profile.id) != null;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 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..f48f66f 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;
@@ -957,8 +924,7 @@
final List<UserInfo> activeUsers = mUm.getUsers();
for (UserInfo userInfo : activeUsers) {
int userId = userInfo.getUserHandle().getIdentifier();
- if (isNASMigrationDone(userId)
- || userInfo.isManagedProfile() || userInfo.isCloneProfile()) {
+ if (isNASMigrationDone(userId) || isProfileUser(userInfo)) {
continue;
}
List<ComponentName> allowedComponents = mAssistants.getAllowedComponents(userId);
@@ -989,6 +955,17 @@
Settings.Secure.NAS_SETTINGS_UPDATED, 0, userId) == 1);
}
+ boolean isProfileUser(UserInfo userInfo) {
+ if (privateSpaceFlagsEnabled()) {
+ return userInfo.isProfile() && hasParent(userInfo);
+ }
+ return userInfo.isManagedProfile() || userInfo.isCloneProfile();
+ }
+
+ boolean hasParent(UserInfo profile) {
+ return mUmInternal.getProfileParentId(profile.id) != profile.id;
+ }
+
protected void setDefaultAssistantForUser(int userId) {
String overrideDefaultAssistantString = DeviceConfig.getProperty(
DeviceConfig.NAMESPACE_SYSTEMUI,
@@ -1097,8 +1074,7 @@
XmlUtils.beginDocument(parser, TAG_NOTIFICATION_POLICY);
boolean migratedManagedServices = false;
UserInfo userInfo = mUmInternal.getUserInfo(userId);
- boolean ineligibleForManagedServices = forRestore &&
- (userInfo.isManagedProfile() || userInfo.isCloneProfile());
+ boolean ineligibleForManagedServices = forRestore && isProfileUser(userInfo);
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
if (ZenModeConfig.ZEN_TAG.equals(parser.getName())) {
@@ -1205,7 +1181,7 @@
}
}
- private static boolean privateSpaceFlagsEnabled() {
+ protected static boolean privateSpaceFlagsEnabled() {
return allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures();
}
@@ -1270,17 +1246,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 +1387,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 +1655,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 +1788,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 +1977,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) {
@@ -2106,7 +1994,7 @@
} else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
mUserProfiles.updateCache(context);
- if (!mUserProfiles.isProfileUser(userId)) {
+ if (!mUserProfiles.isProfileUser(userId, context)) {
// reload per-user settings
mSettingsObserver.update(null);
// Refresh managed services
@@ -2121,7 +2009,7 @@
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
if (userId != USER_NULL) {
mUserProfiles.updateCache(context);
- if (!mUserProfiles.isProfileUser(userId)) {
+ if (!mUserProfiles.isProfileUser(userId, context)) {
allowDefaultApprovedServices(userId);
}
mHistoryManager.onUserAdded(userId);
@@ -2142,7 +2030,7 @@
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_NULL);
mUserProfiles.updateCache(context);
mAssistants.onUserUnlocked(userId);
- if (!mUserProfiles.isProfileUser(userId)) {
+ if (!mUserProfiles.isProfileUser(userId, context)) {
mConditionProviders.onUserUnlocked(userId);
mListeners.onUserUnlocked(userId);
if (!android.app.Flags.modesApi()) {
@@ -2164,8 +2052,6 @@
= Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BADGING);
private final Uri NOTIFICATION_BUBBLES_URI
= Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES);
- private final Uri NOTIFICATION_LIGHT_PULSE_URI
- = Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE);
private final Uri NOTIFICATION_RATE_LIMIT_URI
= Settings.Global.getUriFor(Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE);
private final Uri NOTIFICATION_HISTORY_ENABLED
@@ -2188,10 +2074,6 @@
ContentResolver resolver = getContext().getContentResolver();
resolver.registerContentObserver(NOTIFICATION_BADGING_URI,
false, this, UserHandle.USER_ALL);
- if (!Flags.refactorAttentionHelper()) {
- resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
- false, this, UserHandle.USER_ALL);
- }
resolver.registerContentObserver(NOTIFICATION_RATE_LIMIT_URI,
false, this, UserHandle.USER_ALL);
resolver.registerContentObserver(NOTIFICATION_BUBBLES_URI,
@@ -2218,17 +2100,6 @@
public void update(Uri uri) {
ContentResolver resolver = getContext().getContentResolver();
- if (!Flags.refactorAttentionHelper()) {
- if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
- boolean pulseEnabled = Settings.System.getIntForUser(resolver,
- Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT)
- != 0;
- if (mNotificationPulseEnabled != pulseEnabled) {
- mNotificationPulseEnabled = pulseEnabled;
- updateNotificationPulse();
- }
- }
- }
if (uri == null || NOTIFICATION_RATE_LIMIT_URI.equals(uri)) {
mMaxPackageEnqueueRate = Settings.Global.getFloat(resolver,
Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, mMaxPackageEnqueueRate);
@@ -2347,21 +2218,11 @@
// TODO - replace these methods with new fields in the VisibleForTesting constructor
@VisibleForTesting
- void setAudioManager(AudioManager audioManager) {
- mAudioManager = audioManager;
- }
-
- @VisibleForTesting
void setStrongAuthTracker(StrongAuthTracker strongAuthTracker) {
mStrongAuthTracker = strongAuthTracker;
}
@VisibleForTesting
- void setKeyguardManager(KeyguardManager keyguardManager) {
- mKeyguardManager = keyguardManager;
- }
-
- @VisibleForTesting
ShortcutHelper getShortcutHelper() {
return mShortcutHelper;
}
@@ -2372,33 +2233,6 @@
}
@VisibleForTesting
- VibratorHelper getVibratorHelper() {
- return mVibratorHelper;
- }
-
- @VisibleForTesting
- void setVibratorHelper(VibratorHelper helper) {
- mVibratorHelper = helper;
- }
-
- @VisibleForTesting
- void setHints(int hints) {
- mListenerHints = hints;
- }
-
- @VisibleForTesting
- void setLights(LogicalLight light) {
- mNotificationLight = light;
- mAttentionLight = light;
- mNotificationPulseEnabled = true;
- }
-
- @VisibleForTesting
- void setScreenOn(boolean on) {
- mScreenOn = on;
- }
-
- @VisibleForTesting
int getNotificationRecordCount() {
synchronized (mNotificationLock) {
int count = mNotificationList.size() + mNotificationsByKey.size()
@@ -2446,12 +2280,6 @@
return mNotificationsByKey.get(key);
}
-
- @VisibleForTesting
- void setSystemReady(boolean systemReady) {
- mSystemReady = systemReady;
- }
-
@VisibleForTesting
void setHandler(WorkerHandler handler) {
mHandler = handler;
@@ -2471,13 +2299,8 @@
}
@VisibleForTesting
- void setIsAutomotive(boolean isAutomotive) {
- mIsAutomotive = isAutomotive;
- }
-
- @VisibleForTesting
- void setNotificationEffectsEnabledForAutomotive(boolean isEnabled) {
- mNotificationEffectsEnabledForAutomotive = isEnabled;
+ void setAttentionHelper(NotificationAttentionHelper nah) {
+ mAttentionHelper = nah;
}
@VisibleForTesting
@@ -2486,16 +2309,6 @@
}
@VisibleForTesting
- void setUsageStats(NotificationUsageStats us) {
- mUsageStats = us;
- }
-
- @VisibleForTesting
- void setAccessibilityManager(AccessibilityManager am) {
- mAccessibilityManager = am;
- }
-
- @VisibleForTesting
void setTelecomManager(TelecomManager tm) {
mTelecomManager = tm;
}
@@ -2513,7 +2326,7 @@
DevicePolicyManagerInternal dpm, IUriGrantsManager ugm,
UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
NotificationHistoryManager historyManager, StatsManager statsManager,
- TelephonyManager telephonyManager, ActivityManagerInternal ami,
+ ActivityManagerInternal ami,
MultiRateLimiter toastRateLimiter, PermissionHelper permissionHelper,
UsageStatsManagerInternal usageStatsManagerInternal,
TelecomManager telecomManager, NotificationChannelLogger channelLogger,
@@ -2645,7 +2458,6 @@
extractorNames);
mSnoozeHelper = snoozeHelper;
mGroupHelper = groupHelper;
- mVibratorHelper = new VibratorHelper(getContext());
mHistoryManager = historyManager;
// This is a ManagedServices object that keeps track of the listeners.
@@ -2664,43 +2476,9 @@
mStatusBar.setNotificationDelegate(mNotificationDelegate);
}
- mNotificationLight = lightsManager.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS);
- mAttentionLight = lightsManager.getLight(LightsManager.LIGHT_ID_ATTENTION);
-
- mInCallNotificationUri = Uri.parse("file://" +
- resources.getString(R.string.config_inCallNotificationSound));
- mInCallNotificationAudioAttributes = new AudioAttributes.Builder()
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
- .build();
- mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume);
-
- mUseAttentionLight = resources.getBoolean(R.bool.config_useAttentionLight);
- mHasLight =
- resources.getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed);
-
- // Don't start allowing notifications until the setup wizard has run once.
- // After that, including subsequent boots, init with notifications turned on.
- // This works on the first boot because the setup wizard will toggle this
- // flag at least once and we'll go back to 0 after that.
- if (0 == Settings.Global.getInt(getContext().getContentResolver(),
- Settings.Global.DEVICE_PROVISIONED, 0)) {
- mDisableNotificationEffects = true;
- }
mZenModeHelper.initZenMode();
mInterruptionFilter = mZenModeHelper.getZenModeListenerInterruptionFilter();
- if (mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
- telephonyManager.listen(new PhoneStateListener() {
- @Override
- public void onCallStateChanged(int state, String incomingNumber) {
- if (mCallState == state) return;
- if (DBG) Slog.d(TAG, "Call state changed: " + callStateToString(state));
- mCallState = state;
- }
- }, PhoneStateListener.LISTEN_CALL_STATE);
- }
-
mSettingsObserver = new SettingsObserver(mHandler);
mArchive = new Archive(resources.getInteger(
@@ -2709,11 +2487,6 @@
mIsTelevision = mPackageManagerClient.hasSystemFeature(FEATURE_LEANBACK)
|| mPackageManagerClient.hasSystemFeature(FEATURE_TELEVISION);
- mIsAutomotive =
- mPackageManagerClient.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0);
- mNotificationEffectsEnabledForAutomotive =
- resources.getBoolean(R.bool.config_enableServerNotificationEffectsForAutomotive);
-
mZenModeHelper.setPriorityOnlyDndExemptPackages(getContext().getResources().getStringArray(
com.android.internal.R.array.config_priorityOnlyDndExemptPackages));
@@ -2733,22 +2506,14 @@
mToastRateLimiter = toastRateLimiter;
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager,
+ mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager,
mAccessibilityManager, mPackageManagerClient, userManager, usageStats,
mNotificationManagerPrivate, mZenModeHelper, flagResolver);
- }
// register for various Intents.
// If this is called within a test, make sure to unregister the intent receivers by
// calling onDestroy()
IntentFilter filter = new IntentFilter();
- if (!Flags.refactorAttentionHelper()) {
- filter.addAction(Intent.ACTION_SCREEN_ON);
- filter.addAction(Intent.ACTION_SCREEN_OFF);
- filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
- filter.addAction(Intent.ACTION_USER_PRESENT);
- }
filter.addAction(Intent.ACTION_USER_STOPPED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_USER_ADDED);
@@ -2874,7 +2639,6 @@
new NotificationHistoryManager(getContext(), handler),
mStatsManager = (StatsManager) getContext().getSystemService(
Context.STATS_MANAGER),
- getContext().getSystemService(TelephonyManager.class),
LocalServices.getService(ActivityManagerInternal.class),
createToastRateLimiter(), new PermissionHelper(getContext(),
AppGlobals.getPackageManager(),
@@ -3054,14 +2818,7 @@
@VisibleForTesting
void onBootPhase(int phase, Looper mainLooper) {
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
- // no beeping until we're basically done booting
- mSystemReady = true;
-
- // Grab our optional AudioService
- mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
- mAudioManagerInternal = getLocalService(AudioManagerInternal.class);
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
- mKeyguardManager = getContext().getSystemService(KeyguardManager.class);
mZenModeHelper.onSystemReady();
RoleObserver roleObserver = new RoleObserver(getContext(),
getContext().getSystemService(RoleManager.class),
@@ -3080,9 +2837,7 @@
}
registerNotificationPreferencesPullers();
new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker);
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.onSystemReady();
- }
+ mAttentionHelper.onSystemReady();
} else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
// This observer will force an update when observe is called, causing us to
// bind to listener services.
@@ -6866,33 +6621,6 @@
return null;
}
- private String disableNotificationEffects(NotificationRecord record) {
- if (mDisableNotificationEffects) {
- return "booleanState";
- }
- if ((mListenerHints & HINT_HOST_DISABLE_EFFECTS) != 0) {
- return "listenerHints";
- }
- if (record != null && record.getAudioAttributes() != null) {
- if ((mListenerHints & HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) != 0) {
- if (record.getAudioAttributes().getUsage()
- != AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
- return "listenerNoti";
- }
- }
- if ((mListenerHints & HINT_HOST_DISABLE_CALL_EFFECTS) != 0) {
- if (record.getAudioAttributes().getUsage()
- == AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
- return "listenerCall";
- }
- }
- }
- if (mCallState != TelephonyManager.CALL_STATE_IDLE && !mZenModeHelper.isCall(record)) {
- return "callState";
- }
- return null;
- }
-
// Gets packages that have requested notification permission, and whether that has been
// allowed/denied, for all users on the device.
// Returns a single map containing that info keyed by (uid, package name) for all users.
@@ -7061,33 +6789,10 @@
dumpNotificationRecords(pw, filter);
}
if (!filter.filtered) {
- N = mLights.size();
- if (N > 0) {
- pw.println(" Lights List:");
- for (int i=0; i<N; i++) {
- if (i == N - 1) {
- pw.print(" > ");
- } else {
- pw.print(" ");
- }
- pw.println(mLights.get(i));
- }
- pw.println(" ");
- }
- pw.println(" mUseAttentionLight=" + mUseAttentionLight);
- pw.println(" mHasLight=" + mHasLight);
- pw.println(" mNotificationPulseEnabled=" + mNotificationPulseEnabled);
- pw.println(" mSoundNotificationKey=" + mSoundNotificationKey);
- pw.println(" mVibrateNotificationKey=" + mVibrateNotificationKey);
- pw.println(" mDisableNotificationEffects=" + mDisableNotificationEffects);
- pw.println(" mCallState=" + callStateToString(mCallState));
- pw.println(" mSystemReady=" + mSystemReady);
pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate);
pw.println(" hideSilentStatusBar="
+ mPreferencesHelper.shouldHideSilentStatusIcons());
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.dump(pw, " ", filter);
- }
+ mAttentionHelper.dump(pw, " ", filter);
}
pw.println(" mArchive=" + mArchive.toString());
mArchive.dumpImpl(pw, filter);
@@ -8379,11 +8084,7 @@
boolean wasPosted = removeFromNotificationListsLocked(r);
cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null,
SystemClock.elapsedRealtime());
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.updateLightsLocked();
- } else {
- updateLightsLocked();
- }
+ mAttentionHelper.updateLightsLocked();
if (isSnoozable(r)) {
if (mSnoozeCriterionId != null) {
mAssistants.notifyAssistantSnoozedLocked(r, mSnoozeCriterionId);
@@ -8519,11 +8220,7 @@
cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
mSendDelete, childrenFlagChecker, mReason,
mCancellationElapsedTimeMs);
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.updateLightsLocked();
- } else {
- updateLightsLocked();
- }
+ mAttentionHelper.updateLightsLocked();
if (mShortcutHelper != null) {
mShortcutHelper.maybeListenForShortcutChangesForBubbles(r,
true /* isRemoved */,
@@ -8802,6 +8499,11 @@
r.isUpdate = true;
final boolean isInterruptive = isVisuallyInterruptive(old, r);
r.setTextChanged(isInterruptive);
+ if (updateRankingTime()) {
+ if (isInterruptive) {
+ r.resetRankingTime();
+ }
+ }
}
mNotificationsByKey.put(n.getKey(), r);
@@ -8818,14 +8520,10 @@
int buzzBeepBlinkLoggingCode = 0;
if (!r.isHidden()) {
- if (Flags.refactorAttentionHelper()) {
- buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
+ buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r,
new NotificationAttentionHelper.Signals(
- mUserProfiles.isCurrentProfile(r.getUserId()),
- mListenerHints));
- } else {
- buzzBeepBlinkLoggingCode = buzzBeepBlinkLocked(r);
- }
+ mUserProfiles.isCurrentProfile(r.getUserId()),
+ mListenerHints));
}
if (notification.getSmallIcon() != null) {
@@ -9150,425 +8848,6 @@
}
}
- @VisibleForTesting
- @GuardedBy("mNotificationLock")
- /**
- * Determine whether this notification should attempt to make noise, vibrate, or flash the LED
- * @return buzzBeepBlink - bitfield (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0)
- */
- int buzzBeepBlinkLocked(NotificationRecord record) {
- if (mIsAutomotive && !mNotificationEffectsEnabledForAutomotive) {
- return 0;
- }
- boolean buzz = false;
- boolean beep = false;
- boolean blink = false;
-
- final String key = record.getKey();
-
- // Should this notification make noise, vibe, or use the LED?
- final boolean aboveThreshold =
- mIsAutomotive
- ? record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT
- : record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT;
- // Remember if this notification already owns the notification channels.
- boolean wasBeep = key != null && key.equals(mSoundNotificationKey);
- boolean wasBuzz = key != null && key.equals(mVibrateNotificationKey);
- // These are set inside the conditional if the notification is allowed to make noise.
- boolean hasValidVibrate = false;
- boolean hasValidSound = false;
- boolean sentAccessibilityEvent = false;
-
- // If the notification will appear in the status bar, it should send an accessibility event
- final boolean suppressedByDnd = record.isIntercepted()
- && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0;
- if (!record.isUpdate
- && record.getImportance() > IMPORTANCE_MIN
- && !suppressedByDnd
- && isNotificationForCurrentUser(record)) {
- sendAccessibilityEvent(record);
- sentAccessibilityEvent = true;
- }
-
- if (aboveThreshold && isNotificationForCurrentUser(record)) {
- if (mSystemReady && mAudioManager != null) {
- Uri soundUri = record.getSound();
- hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);
- VibrationEffect vibration = record.getVibration();
- // Demote sound to vibration if vibration missing & phone in vibration mode.
- if (vibration == null
- && hasValidSound
- && (mAudioManager.getRingerModeInternal()
- == AudioManager.RINGER_MODE_VIBRATE)
- && mAudioManager.getStreamVolume(
- AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) == 0) {
- boolean insistent = (record.getFlags() & Notification.FLAG_INSISTENT) != 0;
- vibration = mVibratorHelper.createFallbackVibration(insistent);
- }
- hasValidVibrate = vibration != null;
- boolean hasAudibleAlert = hasValidSound || hasValidVibrate;
- if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) {
- if (!sentAccessibilityEvent) {
- sendAccessibilityEvent(record);
- sentAccessibilityEvent = true;
- }
- if (DBG) Slog.v(TAG, "Interrupting!");
- boolean isInsistentUpdate = isInsistentUpdate(record);
- if (hasValidSound) {
- if (isInsistentUpdate) {
- // don't reset insistent sound, it's jarring
- beep = true;
- } else {
- if (isInCall()) {
- playInCallNotification();
- beep = true;
- } else {
- beep = playSound(record, soundUri);
- }
- if (beep) {
- mSoundNotificationKey = key;
- }
- }
- }
-
- final boolean ringerModeSilent =
- mAudioManager.getRingerModeInternal()
- == AudioManager.RINGER_MODE_SILENT;
- if (!isInCall() && hasValidVibrate && !ringerModeSilent) {
- if (isInsistentUpdate) {
- buzz = true;
- } else {
- buzz = playVibration(record, vibration, hasValidSound);
- if (buzz) {
- mVibrateNotificationKey = key;
- }
- }
- }
-
- // Try to start flash notification event whenever an audible and non-suppressed
- // notification is received
- mAccessibilityManager.startFlashNotificationEvent(getContext(),
- AccessibilityManager.FLASH_REASON_NOTIFICATION,
- record.getSbn().getPackageName());
-
- } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
- hasValidSound = false;
- }
- }
- }
- // If a notification is updated to remove the actively playing sound or vibrate,
- // cancel that feedback now
- if (wasBeep && !hasValidSound) {
- clearSoundLocked();
- }
- if (wasBuzz && !hasValidVibrate) {
- clearVibrateLocked();
- }
-
- // light
- // release the light
- boolean wasShowLights = mLights.remove(key);
- if (canShowLightsLocked(record, aboveThreshold)) {
- mLights.add(key);
- updateLightsLocked();
- if (mUseAttentionLight && mAttentionLight != null) {
- mAttentionLight.pulse();
- }
- blink = true;
- } else if (wasShowLights) {
- updateLightsLocked();
- }
- final int buzzBeepBlink = (buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0);
- if (buzzBeepBlink > 0) {
- // Ignore summary updates because we don't display most of the information.
- if (record.getSbn().isGroup() && record.getSbn().getNotification().isGroupSummary()) {
- if (DEBUG_INTERRUPTIVENESS) {
- Slog.v(TAG, "INTERRUPTIVENESS: "
- + record.getKey() + " is not interruptive: summary");
- }
- } else if (record.canBubble()) {
- if (DEBUG_INTERRUPTIVENESS) {
- Slog.v(TAG, "INTERRUPTIVENESS: "
- + record.getKey() + " is not interruptive: bubble");
- }
- } else {
- record.setInterruptive(true);
- if (DEBUG_INTERRUPTIVENESS) {
- Slog.v(TAG, "INTERRUPTIVENESS: "
- + record.getKey() + " is interruptive: alerted");
- }
- }
- MetricsLogger.action(record.getLogMaker()
- .setCategory(MetricsEvent.NOTIFICATION_ALERT)
- .setType(MetricsEvent.TYPE_OPEN)
- .setSubtype(buzzBeepBlink));
- EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0, 0);
- }
- record.setAudiblyAlerted(buzz || beep);
- return buzzBeepBlink;
- }
-
- @GuardedBy("mNotificationLock")
- boolean canShowLightsLocked(final NotificationRecord record, boolean aboveThreshold) {
- // device lacks light
- if (!mHasLight) {
- return false;
- }
- // user turned lights off globally
- if (!mNotificationPulseEnabled) {
- return false;
- }
- // the notification/channel has no light
- if (record.getLight() == null) {
- return false;
- }
- // unimportant notification
- if (!aboveThreshold) {
- return false;
- }
- // suppressed due to DND
- if ((record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_LIGHTS) != 0) {
- return false;
- }
- // Suppressed because it's a silent update
- final Notification notification = record.getNotification();
- if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) {
- return false;
- }
- // Suppressed because another notification in its group handles alerting
- if (record.getSbn().isGroup() && record.getNotification().suppressAlertingDueToGrouping()) {
- return false;
- }
- // not if in call
- if (isInCall()) {
- return false;
- }
- // check current user
- if (!isNotificationForCurrentUser(record)) {
- return false;
- }
- // Light, but only when the screen is off
- return true;
- }
-
- @GuardedBy("mNotificationLock")
- boolean isInsistentUpdate(final NotificationRecord record) {
- return (Objects.equals(record.getKey(), mSoundNotificationKey)
- || Objects.equals(record.getKey(), mVibrateNotificationKey))
- && isCurrentlyInsistent();
- }
-
- @GuardedBy("mNotificationLock")
- boolean isCurrentlyInsistent() {
- return isLoopingRingtoneNotification(mNotificationsByKey.get(mSoundNotificationKey))
- || isLoopingRingtoneNotification(mNotificationsByKey.get(mVibrateNotificationKey));
- }
-
- @GuardedBy("mNotificationLock")
- boolean shouldMuteNotificationLocked(final NotificationRecord record) {
- // Suppressed because it's a silent update
- final Notification notification = record.getNotification();
- if (record.isUpdate && (notification.flags & FLAG_ONLY_ALERT_ONCE) != 0) {
- return true;
- }
-
- // Suppressed because a user manually unsnoozed something (or similar)
- if (record.shouldPostSilently()) {
- return true;
- }
-
- // muted by listener
- final String disableEffects = disableNotificationEffects(record);
- if (disableEffects != null) {
- ZenLog.traceDisableEffects(record, disableEffects);
- return true;
- }
-
- // suppressed due to DND
- if (record.isIntercepted()) {
- return true;
- }
-
- // Suppressed because another notification in its group handles alerting
- if (record.getSbn().isGroup()) {
- if (notification.suppressAlertingDueToGrouping()) {
- return true;
- }
- }
-
- // Suppressed for being too recently noisy
- final String pkg = record.getSbn().getPackageName();
- if (mUsageStats.isAlertRateLimited(pkg)) {
- Slog.e(TAG, "Muting recently noisy " + record.getKey());
- return true;
- }
-
- // A different looping ringtone, such as an incoming call is playing
- if (isCurrentlyInsistent() && !isInsistentUpdate(record)) {
- return true;
- }
-
- // Suppressed since it's a non-interruptive update to a bubble-suppressed notification
- final boolean isBubbleOrOverflowed = record.canBubble() && (record.isFlagBubbleRemoved()
- || record.getNotification().isBubbleNotification());
- if (record.isUpdate && !record.isInterruptive() && isBubbleOrOverflowed
- && record.getNotification().getBubbleMetadata() != null) {
- if (record.getNotification().getBubbleMetadata().isNotificationSuppressed()) {
- return true;
- }
- }
-
- return false;
- }
-
- @GuardedBy("mNotificationLock")
- private boolean isLoopingRingtoneNotification(final NotificationRecord playingRecord) {
- if (playingRecord != null) {
- if (playingRecord.getAudioAttributes().getUsage() == USAGE_NOTIFICATION_RINGTONE
- && (playingRecord.getNotification().flags & FLAG_INSISTENT) != 0) {
- return true;
- }
- }
- return false;
- }
-
- private boolean playSound(final NotificationRecord record, Uri soundUri) {
- final boolean shouldPlay;
- if (focusExclusiveWithRecording()) {
- // flagged path
- shouldPlay = mAudioManager.shouldNotificationSoundPlay(record.getAudioAttributes());
- } else {
- // legacy path
- // play notifications if there is no user of exclusive audio focus
- // and the stream volume is not 0 (non-zero volume implies not silenced by SILENT or
- // VIBRATE ringer mode)
- shouldPlay = !mAudioManager.isAudioFocusExclusive()
- && (mAudioManager.getStreamVolume(
- AudioAttributes.toLegacyStreamType(record.getAudioAttributes())) != 0);
- }
- if (!shouldPlay) {
- if (DBG) Slog.v(TAG, "Not playing sound " + soundUri + " due to focus/volume");
- return false;
- }
-
- boolean looping = (record.getNotification().flags & FLAG_INSISTENT) != 0;
- final long identity = Binder.clearCallingIdentity();
- try {
- final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
- if (player != null) {
- if (DBG) {
- Slog.v(TAG, "Playing sound " + soundUri
- + " with attributes " + record.getAudioAttributes());
- }
- player.playAsync(soundUri, record.getSbn().getUser(), looping,
- record.getAudioAttributes(), 1.0f);
- return true;
- }
- } catch (RemoteException e) {
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- return false;
- }
-
- private boolean playVibration(final NotificationRecord record, final VibrationEffect effect,
- boolean delayVibForSound) {
- // Escalate privileges so we can use the vibrator even if the
- // notifying app does not have the VIBRATE permission.
- final long identity = Binder.clearCallingIdentity();
- try {
- if (delayVibForSound) {
- new Thread(() -> {
- // delay the vibration by the same amount as the notification sound
- final int waitMs = mAudioManager.getFocusRampTimeMs(
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
- record.getAudioAttributes());
- if (DBG) {
- Slog.v(TAG, "Delaying vibration for notification "
- + record.getKey() + " by " + waitMs + "ms");
- }
- try {
- Thread.sleep(waitMs);
- } catch (InterruptedException e) { }
- // Notifications might be canceled before it actually vibrates due to waitMs,
- // so need to check that the notification is still valid for vibrate.
- synchronized (mNotificationLock) {
- if (mNotificationsByKey.get(record.getKey()) != null) {
- if (record.getKey().equals(mVibrateNotificationKey)) {
- vibrate(record, effect, true);
- } else {
- if (DBG) {
- Slog.v(TAG, "No vibration for notification "
- + record.getKey() + ": a new notification is "
- + "vibrating, or effects were cleared while waiting");
- }
- }
- } else {
- Slog.w(TAG, "No vibration for canceled notification "
- + record.getKey());
- }
- }
- }).start();
- } else {
- vibrate(record, effect, false);
- }
- return true;
- } finally{
- Binder.restoreCallingIdentity(identity);
- }
- }
-
- private void vibrate(NotificationRecord record, VibrationEffect effect, boolean delayed) {
- // We need to vibrate as "android" so we can breakthrough DND. VibratorManagerService
- // doesn't have a concept of vibrating on an app's behalf, so add the app information
- // to the reason so we can still debug from bugreports
- String reason = "Notification (" + record.getSbn().getOpPkg() + " "
- + record.getSbn().getUid() + ") " + (delayed ? "(Delayed)" : "");
- mVibratorHelper.vibrate(effect, record.getAudioAttributes(), reason);
- }
-
- private boolean isNotificationForCurrentUser(NotificationRecord record) {
- final int currentUser;
- final long token = Binder.clearCallingIdentity();
- try {
- currentUser = ActivityManager.getCurrentUser();
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- return (record.getUserId() == UserHandle.USER_ALL ||
- record.getUserId() == currentUser ||
- mUserProfiles.isCurrentProfile(record.getUserId()));
- }
-
- protected void playInCallNotification() {
- final ContentResolver cr = getContext().getContentResolver();
- if (mAudioManager.getRingerModeInternal() == AudioManager.RINGER_MODE_NORMAL
- && Settings.Secure.getIntForUser(cr,
- Settings.Secure.IN_CALL_NOTIFICATION_ENABLED, 1, cr.getUserId()) != 0) {
- new Thread() {
- @Override
- public void run() {
- final long identity = Binder.clearCallingIdentity();
- try {
- final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
- if (player != null) {
- if (mCallNotificationToken != null) {
- player.stop(mCallNotificationToken);
- }
- mCallNotificationToken = new Binder();
- player.play(mCallNotificationToken, mInCallNotificationUri,
- mInCallNotificationAudioAttributes,
- mInCallNotificationVolume, false);
- }
- } catch (RemoteException e) {
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
- }.start();
- }
- }
-
@GuardedBy("mToastQueue")
void showNextToastLocked(boolean lastToastWasTextRecord) {
if (mIsCurrentToastShown) {
@@ -9840,13 +9119,10 @@
|| interruptiveChanged;
if (interceptBefore && !record.isIntercepted()
&& record.isNewEnoughForAlerting(System.currentTimeMillis())) {
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.buzzBeepBlinkLocked(record,
- new NotificationAttentionHelper.Signals(
- mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints));
- } else {
- buzzBeepBlinkLocked(record);
- }
+
+ mAttentionHelper.buzzBeepBlinkLocked(record,
+ new NotificationAttentionHelper.Signals(mUserProfiles.isCurrentProfile(
+ record.getUserId()), mListenerHints));
// Log alert after change in intercepted state to Zen Log as well
ZenLog.traceAlertOnUpdatedIntercept(record);
@@ -10113,37 +9389,6 @@
return (x < low) ? low : ((x > high) ? high : x);
}
- void sendAccessibilityEvent(NotificationRecord record) {
- if (!mAccessibilityManager.isEnabled()) {
- return;
- }
-
- final Notification notification = record.getNotification();
- final CharSequence packageName = record.getSbn().getPackageName();
- final AccessibilityEvent event =
- AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
- event.setPackageName(packageName);
- event.setClassName(Notification.class.getName());
- final int visibilityOverride = record.getPackageVisibilityOverride();
- final int notifVisibility = visibilityOverride == NotificationManager.VISIBILITY_NO_OVERRIDE
- ? notification.visibility : visibilityOverride;
- final int userId = record.getUser().getIdentifier();
- final boolean needPublic = userId >= 0 && mKeyguardManager.isDeviceLocked(userId);
- if (needPublic && notifVisibility != Notification.VISIBILITY_PUBLIC) {
- // Emit the public version if we're on the lockscreen and this notification isn't
- // publicly visible.
- event.setParcelableData(notification.publicVersion);
- } else {
- event.setParcelableData(notification);
- }
- final CharSequence tickerText = notification.tickerText;
- if (!TextUtils.isEmpty(tickerText)) {
- event.getText().add(tickerText);
- }
-
- mAccessibilityManager.sendAccessibilityEvent(event);
- }
-
/**
* Removes all NotificationsRecords with the same key as the given notification record
* from both lists. Do not call this method while iterating over either list.
@@ -10228,22 +9473,7 @@
}
}
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.clearEffectsLocked(canceledKey);
- } else {
- // sound
- if (canceledKey.equals(mSoundNotificationKey)) {
- clearSoundLocked();
- }
-
- // vibrate
- if (canceledKey.equals(mVibrateNotificationKey)) {
- clearVibrateLocked();
- }
-
- // light
- mLights.remove(canceledKey);
- }
+ mAttentionHelper.clearEffectsLocked(canceledKey);
}
// Record usage stats
@@ -10592,11 +9822,7 @@
cancellationElapsedTimeMs);
}
}
- if (Flags.refactorAttentionHelper()) {
- mAttentionHelper.updateLightsLocked();
- } else {
- updateLightsLocked();
- }
+ mAttentionHelper.updateLightsLocked();
}
}
@@ -10745,37 +9971,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 +10169,6 @@
}
}
- private void updateNotificationPulse() {
- synchronized (mNotificationLock) {
- updateLightsLocked();
- }
- }
-
protected boolean isCallingUidSystem() {
final int uid = Binder.getCallingUid();
return uid == Process.SYSTEM_UID;
@@ -11350,18 +10539,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 596a0b0..4c95e83 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -162,9 +162,8 @@
public class LauncherAppsService extends SystemService {
private static final String WM_TRACE_DIR = "/data/misc/wmtrace/";
private static final String VC_FILE_SUFFIX = ".vc";
- // TODO(b/310027945): Update the intent name.
private static final String PS_SETTINGS_INTENT =
- "com.android.settings.action.PRIVATE_SPACE_SETUP_FLOW";
+ "com.android.settings.action.OPEN_PRIVATE_SPACE_SETTINGS";
private static final Set<PosixFilePermission> WM_TRACE_FILE_PERMISSIONS = Set.of(
PosixFilePermission.OWNER_WRITE,
@@ -1801,15 +1800,26 @@
Slog.e(TAG, "Caller cannot access hidden profiles");
return null;
}
+ final int callingUser = getCallingUserId();
+ final int callingUid = getCallingUid();
final long identity = Binder.clearCallingIdentity();
try {
Intent psSettingsIntent = new Intent(PS_SETTINGS_INTENT);
psSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
- final PendingIntent pi = PendingIntent.getActivity(mContext,
+ List<ResolveInfo> ri = mPackageManagerInternal.queryIntentActivities(
+ psSettingsIntent,
+ psSettingsIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ PackageManager.MATCH_SYSTEM_ONLY, callingUid, callingUser);
+ if (ri.isEmpty()) {
+ return null;
+ }
+ final PendingIntent pi = PendingIntent.getActivityAsUser(mContext,
/* requestCode */ 0,
psSettingsIntent,
- PendingIntent.FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT,
+ null,
+ UserHandle.of(callingUser));
return pi == null ? null : pi.getIntentSender();
} finally {
Binder.restoreCallingIdentity(identity);
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index ec98fff..e2f4d18 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -293,9 +293,26 @@
return START_PERMISSION_DENIED;
}
- Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
-
try {
+ boolean openAppDetailsIfOngoingUnarchival = getAppOpsManager().checkOp(
+ AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName)
+ == MODE_ALLOWED;
+ if (openAppDetailsIfOngoingUnarchival) {
+ PackageInstaller.SessionInfo activeUnarchivalSession = getActiveUnarchivalSession(
+ packageName, userId);
+ if (activeUnarchivalSession != null) {
+ mPm.mHandler.post(() -> {
+ Slog.i(TAG, "Opening app details page for ongoing unarchival of: "
+ + packageName);
+ getLauncherApps().startPackageInstallerSessionDetailsActivity(
+ activeUnarchivalSession, null, null);
+ });
+ return START_ABORTED;
+ }
+ }
+
+ Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
+
requestUnarchive(packageName, callerPackageName,
getOrCreateLauncherListener(userId, packageName),
UserHandle.of(userId),
@@ -793,8 +810,27 @@
}
}
- mPm.mHandler.post(
- () -> unarchiveInternal(packageName, userHandle, installerPackage, draftSessionId));
+ mPm.mHandler.post(() -> {
+ Slog.i(TAG, "Starting app unarchival for: " + packageName);
+ unarchiveInternal(packageName, userHandle, installerPackage,
+ draftSessionId);
+ });
+ }
+
+ @Nullable
+ private PackageInstaller.SessionInfo getActiveUnarchivalSession(String packageName,
+ int userId) {
+ List<PackageInstaller.SessionInfo> activeSessions =
+ mPm.mInstallerService.getAllSessions(userId).getList();
+ for (int idx = 0; idx < activeSessions.size(); idx++) {
+ PackageInstaller.SessionInfo activeSession = activeSessions.get(idx);
+ if (activeSession.appPackageName.equals(packageName)
+ && activeSession.userId == userId && activeSession.active
+ && activeSession.isUnarchival()) {
+ return activeSession;
+ }
+ }
+ return null;
}
private void requestUnarchiveConfirmation(String packageName, IntentSender statusReceiver,
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 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/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 88e7596..f5ac830 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1717,20 +1717,26 @@
return false;
}
- if (android.multiuser.Flags.showSetScreenLockDialog()) {
- // Show the prompt to set a new screen lock if the device does not have one
- final KeyguardManager km = mContext.getSystemService(KeyguardManager.class);
- if (km != null && !km.isDeviceSecure()) {
- Intent setScreenLockPromptIntent =
- SetScreenLockDialogActivity
- .createBaseIntent(LAUNCH_REASON_DISABLE_QUIET_MODE);
- setScreenLockPromptIntent.putExtra(EXTRA_ORIGIN_USER_ID, userId);
- mContext.startActivity(setScreenLockPromptIntent);
- return false;
- }
+ final KeyguardManager km = mContext.getSystemService(KeyguardManager.class);
+ if (km != null && km.isDeviceSecure()) {
+ showConfirmCredentialToDisableQuietMode(userId, target, callingPackage);
+ return false;
+ } else if (km != null && !km.isDeviceSecure()
+ && android.multiuser.Flags.showSetScreenLockDialog()
+ // TODO(b/330720545): Add a better way to accomplish this, also use it
+ // to block profile creation w/o device credentials present.
+ && Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0, userId) == 1) {
+ Intent setScreenLockPromptIntent =
+ SetScreenLockDialogActivity
+ .createBaseIntent(LAUNCH_REASON_DISABLE_QUIET_MODE);
+ setScreenLockPromptIntent.putExtra(EXTRA_ORIGIN_USER_ID, userId);
+ mContext.startActivity(setScreenLockPromptIntent);
+ return false;
+ } else {
+ Slog.w(LOG_TAG, "Allowing profile unlock even when device credentials "
+ + "are not set for user " + userId);
}
- showConfirmCredentialToDisableQuietMode(userId, target, callingPackage);
- return false;
}
}
final boolean hasUnifiedChallenge =
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 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/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/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
index d0b46f5..a3aa7de 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
@@ -351,6 +351,29 @@
assertEquals(1, dest.size());
assertEquals(availableLocales.get(0), dest.get(0)); // "sr-Latn-RS"
}
+ // Locale with deprecated subtag, e.g. CS for Serbia and Montenegro, should not win
+ // even if the other available locale doesn't have explicit script / country.
+ // On Android, users don't normally use deprecated subtags unless the application requests.
+ {
+ final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-RS");
+ final ArrayList<Locale> availableLocales = new ArrayList<>();
+ availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS"));
+ availableLocales.add(Locale.forLanguageTag("sr-RS"));
+ final ArrayList<Locale> dest = new ArrayList<>();
+ LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
+ assertEquals(1, dest.size());
+ assertEquals(availableLocales.get(1), dest.get(0)); // "sr-RS"
+ }
+ {
+ final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-RS");
+ final ArrayList<Locale> availableLocales = new ArrayList<>();
+ availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS"));
+ availableLocales.add(Locale.forLanguageTag("sr"));
+ final ArrayList<Locale> dest = new ArrayList<>();
+ LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
+ assertEquals(1, dest.size());
+ assertEquals(availableLocales.get(1), dest.get(0)); // "sr"
+ }
}
@Test
diff --git a/services/tests/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/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 05b6c90..e5c42082 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -20,10 +20,12 @@
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
import static com.android.server.notification.ManagedServices.APPROVAL_BY_COMPONENT;
import static com.android.server.notification.ManagedServices.APPROVAL_BY_PACKAGE;
+import static com.android.server.notification.NotificationManagerService.privateSpaceFlagsEnabled;
import static com.google.common.truth.Truth.assertThat;
@@ -1803,7 +1805,7 @@
@Test
public void testInfoIsPermittedForProfile_notProfile() {
- when(mUserProfiles.isProfileUser(anyInt())).thenReturn(false);
+ when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(false);
IInterface service = mock(IInterface.class);
when(service.asBinder()).thenReturn(mock(IBinder.class));
@@ -1817,7 +1819,7 @@
@Test
public void testInfoIsPermittedForProfile_profileAndDpmAllows() {
- when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true);
+ when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(true);
when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(true);
IInterface service = mock(IInterface.class);
@@ -1833,7 +1835,7 @@
@Test
public void testInfoIsPermittedForProfile_profileAndDpmDenies() {
- when(mUserProfiles.isProfileUser(anyInt())).thenReturn(true);
+ when(mUserProfiles.isProfileUser(anyInt(), any(Context.class))).thenReturn(true);
when(mDpm.isNotificationListenerServicePermitted(anyString(), anyInt())).thenReturn(false);
IInterface service = mock(IInterface.class);
@@ -1853,20 +1855,29 @@
UserInfo profile = new UserInfo(ActivityManager.getCurrentUser(), "current", 0);
profile.userType = USER_TYPE_FULL_SECONDARY;
users.add(profile);
- UserInfo managed = new UserInfo(12, "12", 0);
+ UserInfo managed = new UserInfo(12, "12", UserInfo.FLAG_PROFILE);
managed.userType = USER_TYPE_PROFILE_MANAGED;
users.add(managed);
- UserInfo clone = new UserInfo(13, "13", 0);
+ UserInfo clone = new UserInfo(13, "13", UserInfo.FLAG_PROFILE);
clone.userType = USER_TYPE_PROFILE_CLONE;
users.add(clone);
+ UserInfo privateProfile = new UserInfo(14, "14", UserInfo.FLAG_PROFILE);
+ if (privateSpaceFlagsEnabled()) {
+ privateProfile.userType = USER_TYPE_PROFILE_PRIVATE;
+ users.add(privateProfile);
+ }
when(mUm.getProfiles(ActivityManager.getCurrentUser())).thenReturn(users);
+ when(mUm.getProfileParent(anyInt())).thenReturn(new UserInfo(0, "primary", 0));
ManagedServices.UserProfiles profiles = new ManagedServices.UserProfiles();
profiles.updateCache(mContext);
- assertFalse(profiles.isProfileUser(ActivityManager.getCurrentUser()));
- assertTrue(profiles.isProfileUser(12));
- assertTrue(profiles.isProfileUser(13));
+ assertFalse(profiles.isProfileUser(ActivityManager.getCurrentUser(), mContext));
+ assertTrue(profiles.isProfileUser(12, mContext));
+ assertTrue(profiles.isProfileUser(13, mContext));
+ if (privateSpaceFlagsEnabled()) {
+ assertTrue(profiles.isProfileUser(14, mContext));
+ }
}
@Test
@@ -2015,7 +2026,7 @@
@Test
public void isComponentEnabledForCurrentProfiles_profileUserId() {
final int profileUserId = 10;
- when(mUserProfiles.isProfileUser(profileUserId)).thenReturn(true);
+ when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
// Only approve for parent user (0)
mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", 0, true);
@@ -2028,7 +2039,7 @@
@Test
public void isComponentEnabledForCurrentProfiles_profileUserId_NAS() {
final int profileUserId = 10;
- when(mUserProfiles.isProfileUser(profileUserId)).thenReturn(true);
+ when(mUserProfiles.isProfileUser(profileUserId, mContext)).thenReturn(true);
// Do not rebind for parent users (NAS use-case)
ManagedServices service = spy(mService);
when(service.allowRebindForParentUser()).thenReturn(false);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 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..ef879ee 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;
@@ -81,6 +82,7 @@
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS;
@@ -101,7 +103,6 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
-
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER;
import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER;
@@ -111,11 +112,11 @@
import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_ADJUSTED;
import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_POSTED;
import static com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent.NOTIFICATION_UPDATED;
-
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
-
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
@@ -124,7 +125,6 @@
import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
-
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.isNull;
@@ -132,27 +132,7 @@
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
+import static org.mockito.Mockito.*;
import android.Manifest;
import android.annotation.Nullable;
@@ -207,7 +187,6 @@
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.media.AudioManager;
-import android.media.IRingtonePlayer;
import android.media.session.MediaSession;
import android.net.Uri;
import android.os.Binder;
@@ -246,7 +225,6 @@
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
import android.telecom.TelecomManager;
-import android.telephony.TelephonyManager;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
@@ -260,10 +238,8 @@
import android.util.Pair;
import android.util.Xml;
import android.widget.RemoteViews;
-
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-
import com.android.internal.R;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.config.sysui.TestableFlagResolver;
@@ -294,13 +270,10 @@
import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;
-
import com.google.android.collect.Lists;
import com.google.common.collect.ImmutableList;
-
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -439,6 +412,8 @@
private NotificationChannel mTestNotificationChannel = new NotificationChannel(
TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+ NotificationChannel mSilentChannel = new NotificationChannel("low", "low", IMPORTANCE_LOW);
+
private static final int NOTIFICATION_LOCATION_UNKNOWN = 0;
private static final String VALID_CONVO_SHORTCUT_ID = "shortcut";
@@ -494,6 +469,9 @@
@Mock
StatusBarManagerInternal mStatusBar;
+ @Mock
+ NotificationAttentionHelper mAttentionHelper;
+
private NotificationManagerService.WorkerHandler mWorkerHandler;
private class TestableToastCallback extends ITransientNotification.Stub {
@@ -661,7 +639,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 +673,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 +694,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 +765,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 +1015,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 +1790,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,
@@ -5619,8 +5581,7 @@
+ "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />"
+ "</dnd_apps>"
+ "</notification-policy>";
- UserInfo ui = new UserInfo();
- ui.id = 10;
+ UserInfo ui = new UserInfo(10, "Clone", UserInfo.FLAG_PROFILE);
ui.userType = USER_TYPE_PROFILE_CLONE;
when(mUmInternal.getUserInfo(10)).thenReturn(ui);
mService.readPolicyXml(
@@ -5646,8 +5607,7 @@
+ "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />"
+ "</dnd_apps>"
+ "</notification-policy>";
- UserInfo ui = new UserInfo();
- ui.id = 10;
+ UserInfo ui = new UserInfo(10, "Work", UserInfo.FLAG_PROFILE);
ui.userType = USER_TYPE_PROFILE_MANAGED;
when(mUmInternal.getUserInfo(10)).thenReturn(ui);
mService.readPolicyXml(
@@ -5660,6 +5620,34 @@
}
@Test
+ public void testReadPolicyXml_doesNotRestoreManagedServicesForPrivateUser() throws Exception {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ENABLE_PRIVATE_SPACE_FEATURES);
+ final String policyXml = "<notification-policy version=\"1\">"
+ + "<ranking></ranking>"
+ + "<enabled_listeners>"
+ + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />"
+ + "</enabled_listeners>"
+ + "<enabled_assistants>"
+ + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />"
+ + "</enabled_assistants>"
+ + "<dnd_apps>"
+ + "<service_listing approved=\"test\" user=\"10\" primary=\"true\" />"
+ + "</dnd_apps>"
+ + "</notification-policy>";
+ UserInfo ui = new UserInfo(10, "Private", UserInfo.FLAG_PROFILE);
+ ui.userType = USER_TYPE_PROFILE_PRIVATE;
+ when(mUmInternal.getUserInfo(10)).thenReturn(ui);
+ mService.readPolicyXml(
+ new BufferedInputStream(new ByteArrayInputStream(policyXml.getBytes())),
+ true,
+ 10);
+ verify(mListeners, never()).readXml(any(), any(), eq(true), eq(10));
+ verify(mConditionProviders, never()).readXml(any(), any(), eq(true), eq(10));
+ verify(mAssistants, never()).readXml(any(), any(), eq(true), eq(10));
+ }
+
+ @Test
public void testReadPolicyXml_restoresManagedServicesForNonManagedUser() throws Exception {
final String policyXml = "<notification-policy version=\"1\">"
+ "<ranking></ranking>"
@@ -10105,13 +10093,6 @@
@Test
public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped()
throws RemoteException {
- IRingtonePlayer mockPlayer = mock(IRingtonePlayer.class);
- when(mAudioManager.getRingtonePlayer()).thenReturn(mockPlayer);
- // Set up volume to be above 0, and for AudioManager to signal playback should happen,
- // for the sound to actually play
- when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
- when(mAudioManager.shouldNotificationSoundPlay(any(android.media.AudioAttributes.class)))
- .thenReturn(true);
setUpPrefsForBubbles(PKG, mUid,
true /* global */,
@@ -10130,25 +10111,7 @@
waitForIdle();
// Check audio is stopped
- verify(mockPlayer).stopAsync();
- }
-
- @Test
- public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped_NAHRefactor()
- throws Exception {
- // TODO b/291907312: remove feature flag
- mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER);
- // Cleanup NMS before re-initializing
- if (mService != null) {
- try {
- mService.onDestroy();
- } catch (IllegalStateException | IllegalArgumentException e) {
- // can throw if a broadcast receiver was never registered
- }
- }
- initNMS();
-
- testOnBubbleMetadataChangedToSuppressNotification_soundStopped();
+ verify(mAttentionHelper).clearEffectsLocked(nr.getKey());
}
@Test
@@ -14775,6 +14738,110 @@
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/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.
*