Merge "Fix handling of bad image previews." into rvc-qpr-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index aeed188..36ccaf9 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1120,6 +1120,10 @@
if (isDeviceProvisioningPackage(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
+
+ if (mInjector.isWellbeingPackage(packageName)) {
+ return STANDBY_BUCKET_WORKING_SET;
+ }
}
// Check this last, as it can be the most expensive check
@@ -1929,6 +1933,7 @@
*/
@GuardedBy("mPowerWhitelistedApps")
private final ArraySet<String> mPowerWhitelistedApps = new ArraySet<>();
+ private String mWellbeingApp = null;
Injector(Context context, Looper looper) {
mContext = context;
@@ -1962,6 +1967,9 @@
if (activityManager.isLowRamDevice() || ActivityManager.isSmallBatteryDevice()) {
mAutoRestrictedBucketDelayMs = 12 * ONE_HOUR;
}
+
+ final PackageManager packageManager = mContext.getPackageManager();
+ mWellbeingApp = packageManager.getWellbeingPackageName();
}
mBootPhase = phase;
}
@@ -2006,6 +2014,14 @@
}
}
+ /**
+ * Returns {@code true} if the supplied package is the wellbeing app. Otherwise,
+ * returns {@code false}.
+ */
+ boolean isWellbeingPackage(String packageName) {
+ return mWellbeingApp != null && mWellbeingApp.equals(packageName);
+ }
+
void updatePowerWhitelistCache() {
try {
// Don't call out to DeviceIdleController with the lock held.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 8d64661..b47d44d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -3250,12 +3250,6 @@
sendMessage(H.CLEAN_UP_CONTEXT, cci);
}
- @Override
- public void handleFixedRotationAdjustments(@NonNull IBinder token,
- @Nullable FixedRotationAdjustments fixedRotationAdjustments) {
- handleFixedRotationAdjustments(token, fixedRotationAdjustments, null /* overrideConfig */);
- }
-
/**
* Applies the rotation adjustments to override display information in resources belong to the
* provided token. If the token is activity token, the adjustments also apply to application
@@ -3265,51 +3259,39 @@
* @param fixedRotationAdjustments The information to override the display adjustments of
* corresponding resources. If it is null, the exiting override
* will be cleared.
- * @param overrideConfig The override configuration of activity. It is used to override
- * application configuration. If it is non-null, it means the token is
- * confirmed as activity token. Especially when launching new activity,
- * {@link #mActivities} hasn't put the new token.
*/
- private void handleFixedRotationAdjustments(@NonNull IBinder token,
- @Nullable FixedRotationAdjustments fixedRotationAdjustments,
- @Nullable Configuration overrideConfig) {
- // The element of application configuration override is set only if the application
- // adjustments are needed, because activity already has its own override configuration.
- final Configuration[] appConfigOverride;
- final Consumer<DisplayAdjustments> override;
- if (fixedRotationAdjustments != null) {
- appConfigOverride = new Configuration[1];
- override = displayAdjustments -> {
- displayAdjustments.setFixedRotationAdjustments(fixedRotationAdjustments);
- if (appConfigOverride[0] != null) {
- displayAdjustments.getConfiguration().updateFrom(appConfigOverride[0]);
- }
- };
- } else {
- appConfigOverride = null;
- override = null;
- }
+ @Override
+ public void handleFixedRotationAdjustments(@NonNull IBinder token,
+ @Nullable FixedRotationAdjustments fixedRotationAdjustments) {
+ final Consumer<DisplayAdjustments> override = fixedRotationAdjustments != null
+ ? displayAdjustments -> displayAdjustments
+ .setFixedRotationAdjustments(fixedRotationAdjustments)
+ : null;
if (!mResourcesManager.overrideTokenDisplayAdjustments(token, override)) {
// No resources are associated with the token.
return;
}
- if (overrideConfig == null) {
- final ActivityClientRecord r = mActivities.get(token);
- if (r == null) {
- // It is not an activity token. Nothing to do for application.
- return;
- }
- overrideConfig = r.overrideConfig;
- }
- if (appConfigOverride != null) {
- appConfigOverride[0] = overrideConfig;
+ if (mActivities.get(token) == null) {
+ // Nothing to do for application if it is not an activity token.
+ return;
}
- // Apply the last override to application resources for compatibility. Because the Resources
- // of Display can be from application, e.g.
- // applicationContext.getSystemService(DisplayManager.class).getDisplay(displayId)
- // and the deprecated usage:
- // applicationContext.getSystemService(WindowManager.class).getDefaultDisplay();
+ overrideApplicationDisplayAdjustments(token, override);
+ }
+
+ /**
+ * Applies the last override to application resources for compatibility. Because the Resources
+ * of Display can be from application, e.g.
+ * applicationContext.getSystemService(DisplayManager.class).getDisplay(displayId)
+ * and the deprecated usage:
+ * applicationContext.getSystemService(WindowManager.class).getDefaultDisplay();
+ *
+ * @param token The owner and target of the override.
+ * @param override The display adjustments override for application resources. If it is null,
+ * the override of the token will be removed and pop the last one to use.
+ */
+ private void overrideApplicationDisplayAdjustments(@NonNull IBinder token,
+ @Nullable Consumer<DisplayAdjustments> override) {
final Consumer<DisplayAdjustments> appOverride;
if (mActiveRotationAdjustments == null) {
mActiveRotationAdjustments = new ArrayList<>(2);
@@ -3542,8 +3524,13 @@
// The rotation adjustments must be applied before creating the activity, so the activity
// can get the adjusted display info during creation.
if (r.mPendingFixedRotationAdjustments != null) {
- handleFixedRotationAdjustments(r.token, r.mPendingFixedRotationAdjustments,
- r.overrideConfig);
+ // The adjustments should have been set by handleLaunchActivity, so the last one is the
+ // override for activity resources.
+ if (mActiveRotationAdjustments != null && !mActiveRotationAdjustments.isEmpty()) {
+ mResourcesManager.overrideTokenDisplayAdjustments(r.token,
+ mActiveRotationAdjustments.get(
+ mActiveRotationAdjustments.size() - 1).second);
+ }
r.mPendingFixedRotationAdjustments = null;
}
@@ -3582,6 +3569,13 @@
mProfiler.startProfiling();
}
+ if (r.mPendingFixedRotationAdjustments != null) {
+ // The rotation adjustments must be applied before handling configuration, so process
+ // level display metrics can be adjusted.
+ overrideApplicationDisplayAdjustments(r.token, adjustments ->
+ adjustments.setFixedRotationAdjustments(r.mPendingFixedRotationAdjustments));
+ }
+
// Make sure we are running with the most recent config.
handleConfigurationChanged(null, null);
@@ -5777,7 +5771,15 @@
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle configuration changed: "
+ config);
- mResourcesManager.applyConfigurationToResourcesLocked(config, compat);
+ final Resources appResources = mInitialApplication.getResources();
+ if (appResources.hasOverrideDisplayAdjustments()) {
+ // The value of Display#getRealSize will be adjusted by FixedRotationAdjustments,
+ // but Display#getSize refers to DisplayAdjustments#mConfiguration. So the rotated
+ // configuration also needs to set to the adjustments for consistency.
+ appResources.getDisplayAdjustments().getConfiguration().updateFrom(config);
+ }
+ mResourcesManager.applyConfigurationToResourcesLocked(config, compat,
+ appResources.getDisplayAdjustments());
updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
mResourcesManager.getConfiguration().getLocales());
@@ -7390,7 +7392,8 @@
// We need to apply this change to the resources immediately, because upon returning
// the view hierarchy will be informed about it.
if (mResourcesManager.applyConfigurationToResourcesLocked(globalConfig,
- null /* compat */)) {
+ null /* compat */,
+ mInitialApplication.getResources().getDisplayAdjustments())) {
updateLocaleListFromAppContext(mInitialApplication.getApplicationContext(),
mResourcesManager.getConfiguration().getLocales());
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index c51a846..7effbb3 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -187,16 +187,6 @@
private static final String XATTR_INODE_CODE_CACHE = "user.inode_code_cache";
/**
- * Special intent extra that critical system apps can use to hide the notification for a
- * foreground service. This extra should be placed in the intent passed into {@link
- * #startForegroundService(Intent)}.
- *
- * @hide
- */
- private static final String EXTRA_HIDDEN_FOREGROUND_SERVICE =
- "android.intent.extra.HIDDEN_FOREGROUND_SERVICE";
-
- /**
* Map from package name, to preference name, to cached preferences.
*/
@GuardedBy("ContextImpl.class")
@@ -1707,12 +1697,9 @@
try {
validateServiceIntent(service);
service.prepareToLeaveProcess(this);
- final boolean hideForegroundNotification = requireForeground
- && service.getBooleanExtra(EXTRA_HIDDEN_FOREGROUND_SERVICE, false);
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service,
service.resolveTypeIfNeeded(getContentResolver()), requireForeground,
- hideForegroundNotification,
getOpPackageName(), getAttributionTag(), user.getIdentifier());
if (cn != null) {
if (cn.getPackageName().equals("!")) {
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index f98e263..2fe7eea 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -155,8 +155,7 @@
boolean refContentProvider(in IBinder connection, int stableDelta, int unstableDelta);
PendingIntent getRunningServiceControlPanel(in ComponentName service);
ComponentName startService(in IApplicationThread caller, in Intent service,
- in String resolvedType, boolean requireForeground,
- boolean hideForegroundNotification, in String callingPackage,
+ in String resolvedType, boolean requireForeground, in String callingPackage,
in String callingFeatureId, int userId);
@UnsupportedAppUsage
int stopService(in IApplicationThread caller, in Intent service,
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 4cba6ea..273336d 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -40,7 +40,6 @@
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.LruCache;
import android.util.Pair;
import android.util.Slog;
import android.view.Display;
@@ -63,7 +62,6 @@
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.function.Consumer;
-import java.util.function.Predicate;
/** @hide */
public class ResourcesManager {
@@ -130,17 +128,30 @@
}
}
- private static final boolean ENABLE_APK_ASSETS_CACHE = false;
-
/**
- * The ApkAssets we are caching and intend to hold strong references to.
+ * Loads {@link ApkAssets} and caches them to prevent their garbage collection while the
+ * instance is alive and reachable.
*/
- private final LruCache<ApkKey, ApkAssets> mLoadedApkAssets =
- (ENABLE_APK_ASSETS_CACHE) ? new LruCache<>(3) : null;
+ private class ApkAssetsSupplier {
+ final ArrayMap<ApkKey, ApkAssets> mLocalCache = new ArrayMap<>();
+
+ /**
+ * Retrieves the {@link ApkAssets} corresponding to the specified key, caches the ApkAssets
+ * within this instance, and inserts the loaded ApkAssets into the {@link #mCachedApkAssets}
+ * cache.
+ */
+ ApkAssets load(final ApkKey apkKey) throws IOException {
+ ApkAssets apkAssets = mLocalCache.get(apkKey);
+ if (apkAssets == null) {
+ apkAssets = loadApkAssets(apkKey);
+ mLocalCache.put(apkKey, apkAssets);
+ }
+ return apkAssets;
+ }
+ }
/**
- * The ApkAssets that are being referenced in the wild that we can reuse, even if they aren't
- * in our LRU cache. Bonus resources :)
+ * The ApkAssets that are being referenced in the wild that we can reuse.
*/
private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>();
@@ -338,49 +349,78 @@
return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
}
- private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
- throws IOException {
- final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
- ApkAssets apkAssets = null;
- if (mLoadedApkAssets != null) {
- apkAssets = mLoadedApkAssets.get(newKey);
- if (apkAssets != null && apkAssets.isUpToDate()) {
- return apkAssets;
- }
- }
+ private @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException {
+ ApkAssets apkAssets;
// Optimistically check if this ApkAssets exists somewhere else.
- final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey);
- if (apkAssetsRef != null) {
- apkAssets = apkAssetsRef.get();
- if (apkAssets != null && apkAssets.isUpToDate()) {
- if (mLoadedApkAssets != null) {
- mLoadedApkAssets.put(newKey, apkAssets);
+ synchronized (this) {
+ final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(key);
+ if (apkAssetsRef != null) {
+ apkAssets = apkAssetsRef.get();
+ if (apkAssets != null && apkAssets.isUpToDate()) {
+ return apkAssets;
+ } else {
+ // Clean up the reference.
+ mCachedApkAssets.remove(key);
}
-
- return apkAssets;
- } else {
- // Clean up the reference.
- mCachedApkAssets.remove(newKey);
}
}
// We must load this from disk.
- if (overlay) {
- apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), 0 /*flags*/);
+ if (key.overlay) {
+ apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(key.path),
+ 0 /*flags*/);
} else {
- apkAssets = ApkAssets.loadFromPath(path, sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
+ apkAssets = ApkAssets.loadFromPath(key.path,
+ key.sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
}
- if (mLoadedApkAssets != null) {
- mLoadedApkAssets.put(newKey, apkAssets);
+ synchronized (this) {
+ mCachedApkAssets.put(key, new WeakReference<>(apkAssets));
}
- mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
/**
+ * Retrieves a list of apk keys representing the ApkAssets that should be loaded for
+ * AssetManagers mapped to the {@param key}.
+ */
+ private static @NonNull ArrayList<ApkKey> extractApkKeys(@NonNull final ResourcesKey key) {
+ final ArrayList<ApkKey> apkKeys = new ArrayList<>();
+
+ // resDir can be null if the 'android' package is creating a new Resources object.
+ // This is fine, since each AssetManager automatically loads the 'android' package
+ // already.
+ if (key.mResDir != null) {
+ apkKeys.add(new ApkKey(key.mResDir, false /*sharedLib*/, false /*overlay*/));
+ }
+
+ if (key.mSplitResDirs != null) {
+ for (final String splitResDir : key.mSplitResDirs) {
+ apkKeys.add(new ApkKey(splitResDir, false /*sharedLib*/, false /*overlay*/));
+ }
+ }
+
+ if (key.mLibDirs != null) {
+ for (final String libDir : key.mLibDirs) {
+ // Avoid opening files we know do not have resources, like code-only .jar files.
+ if (libDir.endsWith(".apk")) {
+ apkKeys.add(new ApkKey(libDir, true /*sharedLib*/, false /*overlay*/));
+ }
+ }
+ }
+
+ if (key.mOverlayDirs != null) {
+ for (final String idmapPath : key.mOverlayDirs) {
+ apkKeys.add(new ApkKey(idmapPath, false /*sharedLib*/, true /*overlay*/));
+ }
+ }
+
+ return apkKeys;
+ }
+
+ /**
* Creates an AssetManager from the paths within the ResourcesKey.
*
* This can be overridden in tests so as to avoid creating a real AssetManager with
@@ -391,64 +431,38 @@
@VisibleForTesting
@UnsupportedAppUsage
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
+ return createAssetManager(key, /* apkSupplier */ null);
+ }
+
+ /**
+ * Variant of {@link #createAssetManager(ResourcesKey)} that attempts to load ApkAssets
+ * from an {@link ApkAssetsSupplier} if non-null; otherwise ApkAssets are loaded using
+ * {@link #loadApkAssets(ApkKey)}.
+ */
+ private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
+ @Nullable ApkAssetsSupplier apkSupplier) {
final AssetManager.Builder builder = new AssetManager.Builder();
- // resDir can be null if the 'android' package is creating a new Resources object.
- // This is fine, since each AssetManager automatically loads the 'android' package
- // already.
- if (key.mResDir != null) {
+ final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
+ for (int i = 0, n = apkKeys.size(); i < n; i++) {
+ final ApkKey apkKey = apkKeys.get(i);
try {
- builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
- false /*overlay*/));
+ builder.addApkAssets(
+ (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
} catch (IOException e) {
- Log.e(TAG, "failed to add asset path " + key.mResDir);
- return null;
- }
- }
-
- if (key.mSplitResDirs != null) {
- for (final String splitResDir : key.mSplitResDirs) {
- try {
- builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
- false /*overlay*/));
- } catch (IOException e) {
- Log.e(TAG, "failed to add split asset path " + splitResDir);
+ if (apkKey.overlay) {
+ Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e);
+ } else if (apkKey.sharedLib) {
+ Log.w(TAG, String.format(
+ "asset path '%s' does not exist or contains no resources",
+ apkKey.path), e);
+ } else {
+ Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e);
return null;
}
}
}
- if (key.mLibDirs != null) {
- for (final String libDir : key.mLibDirs) {
- if (libDir.endsWith(".apk")) {
- // Avoid opening files we know do not have resources,
- // like code-only .jar files.
- try {
- builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
- false /*overlay*/));
- } catch (IOException e) {
- Log.w(TAG, "Asset path '" + libDir +
- "' does not exist or contains no resources.");
-
- // continue.
- }
- }
- }
- }
-
- if (key.mOverlayDirs != null) {
- for (final String idmapPath : key.mOverlayDirs) {
- try {
- builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
- true /*overlay*/));
- } catch (IOException e) {
- Log.w(TAG, "failed to add overlay path " + idmapPath);
-
- // continue.
- }
- }
- }
-
if (key.mLoaders != null) {
for (final ResourcesLoader loader : key.mLoaders) {
builder.addLoader(loader);
@@ -481,24 +495,6 @@
pw.println("ResourcesManager:");
pw.increaseIndent();
- if (mLoadedApkAssets != null) {
- pw.print("cached apks: total=");
- pw.print(mLoadedApkAssets.size());
- pw.print(" created=");
- pw.print(mLoadedApkAssets.createCount());
- pw.print(" evicted=");
- pw.print(mLoadedApkAssets.evictionCount());
- pw.print(" hit=");
- pw.print(mLoadedApkAssets.hitCount());
- pw.print(" miss=");
- pw.print(mLoadedApkAssets.missCount());
- pw.print(" max=");
- pw.print(mLoadedApkAssets.maxSize());
- } else {
- pw.print("cached apks: 0 [cache disabled]");
- }
- pw.println();
-
pw.print("total apks: ");
pw.println(countLiveReferences(mCachedApkAssets.values()));
@@ -534,11 +530,12 @@
return config;
}
- private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
+ private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
+ @Nullable ApkAssetsSupplier apkSupplier) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
- final AssetManager assets = createAssetManager(key);
+ final AssetManager assets = createAssetManager(key, apkSupplier);
if (assets == null) {
return null;
}
@@ -576,9 +573,18 @@
*/
private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
@NonNull ResourcesKey key) {
+ return findOrCreateResourcesImplForKeyLocked(key, /* apkSupplier */ null);
+ }
+
+ /**
+ * Variant of {@link #findOrCreateResourcesImplForKeyLocked(ResourcesKey)} that attempts to
+ * load ApkAssets from a {@link ApkAssetsSupplier} when creating a new ResourcesImpl.
+ */
+ private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
+ @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
ResourcesImpl impl = findResourcesImplForKeyLocked(key);
if (impl == null) {
- impl = createResourcesImpl(key);
+ impl = createResourcesImpl(key, apkSupplier);
if (impl != null) {
mResourceImpls.put(key, new WeakReference<>(impl));
}
@@ -767,7 +773,7 @@
}
// Now request an actual Resources object.
- return createResources(token, key, classLoader);
+ return createResources(token, key, classLoader, /* apkSupplier */ null);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
@@ -811,18 +817,45 @@
}
/**
+ * Creates an {@link ApkAssetsSupplier} and loads all the ApkAssets required by the {@param key}
+ * into the supplier. This should be done while the lock is not held to prevent performing I/O
+ * while holding the lock.
+ */
+ private @NonNull ApkAssetsSupplier createApkAssetsSupplierNotLocked(@NonNull ResourcesKey key) {
+ Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
+ "ResourcesManager#createApkAssetsSupplierNotLocked");
+ try {
+ final ApkAssetsSupplier supplier = new ApkAssetsSupplier();
+ final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
+ for (int i = 0, n = apkKeys.size(); i < n; i++) {
+ final ApkKey apkKey = apkKeys.get(i);
+ try {
+ supplier.load(apkKey);
+ } catch (IOException e) {
+ Log.w(TAG, String.format("failed to preload asset path '%s'", apkKey.path), e);
+ }
+ }
+ return supplier;
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+ }
+ }
+
+ /**
* Creates a Resources object set with a ResourcesImpl object matching the given key.
*
* @param activityToken The Activity this Resources object should be associated with.
* @param key The key describing the parameters of the ResourcesImpl object.
* @param classLoader The classloader to use for the Resources object.
* If null, {@link ClassLoader#getSystemClassLoader()} is used.
+ * @param apkSupplier The apk assets supplier to use when creating a new ResourcesImpl object.
* @return A Resources object that gets updated when
* {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
* is called.
*/
private @Nullable Resources createResources(@Nullable IBinder activityToken,
- @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
+ @NonNull ResourcesKey key, @NonNull ClassLoader classLoader,
+ @Nullable ApkAssetsSupplier apkSupplier) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
@@ -830,7 +863,7 @@
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
- ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
+ ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
if (resourcesImpl == null) {
return null;
}
@@ -899,7 +932,10 @@
rebaseKeyForActivity(activityToken, key);
}
- return createResources(activityToken, key, classLoader);
+ // Preload the ApkAssets required by the key to prevent performing heavy I/O while the
+ // ResourcesManager lock is held.
+ final ApkAssetsSupplier assetsSupplier = createApkAssetsSupplierNotLocked(key);
+ return createResources(activityToken, key, classLoader, assetsSupplier);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
@@ -970,7 +1006,13 @@
final ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig,
overrideConfig, displayId);
if (newKey != null) {
- updateActivityResources(resources, newKey, false);
+ final ResourcesImpl resourcesImpl =
+ findOrCreateResourcesImplForKeyLocked(newKey);
+ if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
+ // Set the ResourcesImpl, updating it for all users of this Resources
+ // object.
+ resources.setImpl(resourcesImpl);
+ }
}
}
}
@@ -1025,34 +1067,22 @@
return newKey;
}
- private void updateActivityResources(Resources resources, ResourcesKey newKey,
- boolean hasLoader) {
- final ResourcesImpl resourcesImpl;
-
- if (hasLoader) {
- // Loaders always get new Impls because they cannot be shared
- resourcesImpl = createResourcesImpl(newKey);
- } else {
- resourcesImpl = findOrCreateResourcesImplForKeyLocked(newKey);
- }
-
- if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
- // Set the ResourcesImpl, updating it for all users of this Resources
- // object.
- resources.setImpl(resourcesImpl);
- }
- }
-
@TestApi
public final boolean applyConfigurationToResources(@NonNull Configuration config,
@Nullable CompatibilityInfo compat) {
synchronized(this) {
- return applyConfigurationToResourcesLocked(config, compat);
+ return applyConfigurationToResourcesLocked(config, compat, null /* adjustments */);
}
}
public final boolean applyConfigurationToResourcesLocked(@NonNull Configuration config,
- @Nullable CompatibilityInfo compat) {
+ @Nullable CompatibilityInfo compat) {
+ return applyConfigurationToResourcesLocked(config, compat, null /* adjustments */);
+ }
+
+ /** Applies the global configuration to the managed resources. */
+ public final boolean applyConfigurationToResourcesLocked(@NonNull Configuration config,
+ @Nullable CompatibilityInfo compat, @Nullable DisplayAdjustments adjustments) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
"ResourcesManager#applyConfigurationToResourcesLocked");
@@ -1076,6 +1106,11 @@
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
}
+ if (adjustments != null) {
+ // Currently the only case where the adjustment takes effect is to simulate placing
+ // an app in a rotated display.
+ adjustments.adjustGlobalAppMetrics(defaultDisplayMetrics);
+ }
Resources.updateSystemConfiguration(config, defaultDisplayMetrics, compat);
ApplicationPackageManager.configurationChanged();
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index bde3327..018bb2c 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -135,8 +135,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
diff --git a/core/java/android/view/DisplayAdjustments.java b/core/java/android/view/DisplayAdjustments.java
index c726bee..7c01f7a8 100644
--- a/core/java/android/view/DisplayAdjustments.java
+++ b/core/java/android/view/DisplayAdjustments.java
@@ -130,14 +130,16 @@
w = metrics.noncompatWidthPixels;
metrics.noncompatWidthPixels = metrics.noncompatHeightPixels;
metrics.noncompatHeightPixels = w;
+ }
- float x = metrics.xdpi;
- metrics.xdpi = metrics.ydpi;
- metrics.ydpi = x;
-
- x = metrics.noncompatXdpi;
- metrics.noncompatXdpi = metrics.noncompatYdpi;
- metrics.noncompatYdpi = x;
+ /** Adjusts global display metrics that is available to applications. */
+ public void adjustGlobalAppMetrics(@NonNull DisplayMetrics metrics) {
+ final FixedRotationAdjustments rotationAdjustments = mFixedRotationAdjustments;
+ if (rotationAdjustments == null) {
+ return;
+ }
+ metrics.noncompatWidthPixels = metrics.widthPixels = rotationAdjustments.mAppWidth;
+ metrics.noncompatHeightPixels = metrics.heightPixels = rotationAdjustments.mAppHeight;
}
/** Returns the adjusted cutout if available. Otherwise the original cutout is returned. */
@@ -178,7 +180,7 @@
/**
* An application can be launched in different rotation than the real display. This class
- * provides the information to adjust the values returned by {@link #Display}.
+ * provides the information to adjust the values returned by {@link Display}.
* @hide
*/
public static class FixedRotationAdjustments implements Parcelable {
@@ -186,12 +188,24 @@
@Surface.Rotation
final int mRotation;
+ /**
+ * The rotated {@link DisplayInfo#appWidth}. The value cannot be simply swapped according
+ * to rotation because it minus the region of screen decorations.
+ */
+ final int mAppWidth;
+
+ /** The rotated {@link DisplayInfo#appHeight}. */
+ final int mAppHeight;
+
/** Non-null if the device has cutout. */
@Nullable
final DisplayCutout mRotatedDisplayCutout;
- public FixedRotationAdjustments(@Surface.Rotation int rotation, DisplayCutout cutout) {
+ public FixedRotationAdjustments(@Surface.Rotation int rotation, int appWidth, int appHeight,
+ DisplayCutout cutout) {
mRotation = rotation;
+ mAppWidth = appWidth;
+ mAppHeight = appHeight;
mRotatedDisplayCutout = cutout;
}
@@ -199,6 +213,8 @@
public int hashCode() {
int hash = 17;
hash = hash * 31 + mRotation;
+ hash = hash * 31 + mAppWidth;
+ hash = hash * 31 + mAppHeight;
hash = hash * 31 + Objects.hashCode(mRotatedDisplayCutout);
return hash;
}
@@ -210,12 +226,14 @@
}
final FixedRotationAdjustments other = (FixedRotationAdjustments) o;
return mRotation == other.mRotation
+ && mAppWidth == other.mAppWidth && mAppHeight == other.mAppHeight
&& Objects.equals(mRotatedDisplayCutout, other.mRotatedDisplayCutout);
}
@Override
public String toString() {
return "FixedRotationAdjustments{rotation=" + Surface.rotationToString(mRotation)
+ + " appWidth=" + mAppWidth + " appHeight=" + mAppHeight
+ " cutout=" + mRotatedDisplayCutout + "}";
}
@@ -227,12 +245,16 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mRotation);
+ dest.writeInt(mAppWidth);
+ dest.writeInt(mAppHeight);
dest.writeTypedObject(
new DisplayCutout.ParcelableWrapper(mRotatedDisplayCutout), flags);
}
private FixedRotationAdjustments(Parcel in) {
mRotation = in.readInt();
+ mAppWidth = in.readInt();
+ mAppHeight = in.readInt();
final DisplayCutout.ParcelableWrapper cutoutWrapper =
in.readTypedObject(DisplayCutout.ParcelableWrapper.CREATOR);
mRotatedDisplayCutout = cutoutWrapper != null ? cutoutWrapper.get() : null;
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 0b43547..8137883 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -76,6 +76,10 @@
ITYPE_BOTTOM_GESTURES,
ITYPE_LEFT_GESTURES,
ITYPE_RIGHT_GESTURES,
+ ITYPE_TOP_MANDATORY_GESTURES,
+ ITYPE_BOTTOM_MANDATORY_GESTURES,
+ ITYPE_LEFT_MANDATORY_GESTURES,
+ ITYPE_RIGHT_MANDATORY_GESTURES,
ITYPE_TOP_TAPPABLE_ELEMENT,
ITYPE_BOTTOM_TAPPABLE_ELEMENT,
ITYPE_LEFT_DISPLAY_CUTOUT,
@@ -104,20 +108,27 @@
public static final int ITYPE_BOTTOM_GESTURES = 4;
public static final int ITYPE_LEFT_GESTURES = 5;
public static final int ITYPE_RIGHT_GESTURES = 6;
- public static final int ITYPE_TOP_TAPPABLE_ELEMENT = 7;
- public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT = 8;
- public static final int ITYPE_LEFT_DISPLAY_CUTOUT = 9;
- public static final int ITYPE_TOP_DISPLAY_CUTOUT = 10;
- public static final int ITYPE_RIGHT_DISPLAY_CUTOUT = 11;
- public static final int ITYPE_BOTTOM_DISPLAY_CUTOUT = 12;
+ /** Additional gesture inset types that map into {@link Type.MANDATORY_SYSTEM_GESTURES}. */
+ public static final int ITYPE_TOP_MANDATORY_GESTURES = 7;
+ public static final int ITYPE_BOTTOM_MANDATORY_GESTURES = 8;
+ public static final int ITYPE_LEFT_MANDATORY_GESTURES = 9;
+ public static final int ITYPE_RIGHT_MANDATORY_GESTURES = 10;
+
+ public static final int ITYPE_TOP_TAPPABLE_ELEMENT = 11;
+ public static final int ITYPE_BOTTOM_TAPPABLE_ELEMENT = 12;
+
+ public static final int ITYPE_LEFT_DISPLAY_CUTOUT = 13;
+ public static final int ITYPE_TOP_DISPLAY_CUTOUT = 14;
+ public static final int ITYPE_RIGHT_DISPLAY_CUTOUT = 15;
+ public static final int ITYPE_BOTTOM_DISPLAY_CUTOUT = 16;
/** Input method window. */
- public static final int ITYPE_IME = 13;
+ public static final int ITYPE_IME = 17;
/** Additional system decorations inset type. */
- public static final int ITYPE_CLIMATE_BAR = 14;
- public static final int ITYPE_EXTRA_NAVIGATION_BAR = 15;
+ public static final int ITYPE_CLIMATE_BAR = 18;
+ public static final int ITYPE_EXTRA_NAVIGATION_BAR = 19;
static final int LAST_TYPE = ITYPE_EXTRA_NAVIGATION_BAR;
public static final int SIZE = LAST_TYPE + 1;
@@ -493,6 +504,10 @@
return Type.IME;
case ITYPE_TOP_GESTURES:
case ITYPE_BOTTOM_GESTURES:
+ case ITYPE_TOP_MANDATORY_GESTURES:
+ case ITYPE_BOTTOM_MANDATORY_GESTURES:
+ case ITYPE_LEFT_MANDATORY_GESTURES:
+ case ITYPE_RIGHT_MANDATORY_GESTURES:
return Type.MANDATORY_SYSTEM_GESTURES;
case ITYPE_LEFT_GESTURES:
case ITYPE_RIGHT_GESTURES:
@@ -552,6 +567,14 @@
return "ITYPE_LEFT_GESTURES";
case ITYPE_RIGHT_GESTURES:
return "ITYPE_RIGHT_GESTURES";
+ case ITYPE_TOP_MANDATORY_GESTURES:
+ return "ITYPE_TOP_MANDATORY_GESTURES";
+ case ITYPE_BOTTOM_MANDATORY_GESTURES:
+ return "ITYPE_BOTTOM_MANDATORY_GESTURES";
+ case ITYPE_LEFT_MANDATORY_GESTURES:
+ return "ITYPE_LEFT_MANDATORY_GESTURES";
+ case ITYPE_RIGHT_MANDATORY_GESTURES:
+ return "ITYPE_RIGHT_MANDATORY_GESTURES";
case ITYPE_TOP_TAPPABLE_ELEMENT:
return "ITYPE_TOP_TAPPABLE_ELEMENT";
case ITYPE_BOTTOM_TAPPABLE_ELEMENT:
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4a56761..3763728 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1981,11 +1981,7 @@
mCompatibleVisibilityInfo.globalVisibility =
(mCompatibleVisibilityInfo.globalVisibility & ~View.SYSTEM_UI_FLAG_LOW_PROFILE)
| (mAttachInfo.mSystemUiVisibility & View.SYSTEM_UI_FLAG_LOW_PROFILE);
- if (mDispatchedSystemUiVisibility != mCompatibleVisibilityInfo.globalVisibility) {
- mHandler.removeMessages(MSG_DISPATCH_SYSTEM_UI_VISIBILITY);
- mHandler.sendMessage(mHandler.obtainMessage(
- MSG_DISPATCH_SYSTEM_UI_VISIBILITY, mCompatibleVisibilityInfo));
- }
+ dispatchDispatchSystemUiVisibilityChanged(mCompatibleVisibilityInfo);
if (mAttachInfo.mKeepScreenOn != oldScreenOn
|| mAttachInfo.mSystemUiVisibility != params.subtreeSystemUiVisibility
|| mAttachInfo.mHasSystemUiListeners != params.hasSystemUiListeners) {
@@ -2039,9 +2035,30 @@
info.globalVisibility |= systemUiFlag;
info.localChanges &= ~systemUiFlag;
}
- if (mDispatchedSystemUiVisibility != info.globalVisibility) {
+ dispatchDispatchSystemUiVisibilityChanged(info);
+ }
+
+ /**
+ * If the system is forcing showing any system bar, the legacy low profile flag should be
+ * cleared for compatibility.
+ *
+ * @param showTypes {@link InsetsType types} shown by the system.
+ * @param fromIme {@code true} if the invocation is from IME.
+ */
+ private void clearLowProfileModeIfNeeded(@InsetsType int showTypes, boolean fromIme) {
+ final SystemUiVisibilityInfo info = mCompatibleVisibilityInfo;
+ if ((showTypes & Type.systemBars()) != 0 && !fromIme
+ && (info.globalVisibility & SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
+ info.globalVisibility &= ~SYSTEM_UI_FLAG_LOW_PROFILE;
+ info.localChanges |= SYSTEM_UI_FLAG_LOW_PROFILE;
+ dispatchDispatchSystemUiVisibilityChanged(info);
+ }
+ }
+
+ private void dispatchDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
+ if (mDispatchedSystemUiVisibility != args.globalVisibility) {
mHandler.removeMessages(MSG_DISPATCH_SYSTEM_UI_VISIBILITY);
- mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY, info));
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_DISPATCH_SYSTEM_UI_VISIBILITY, args));
}
}
@@ -5008,6 +5025,7 @@
String.format("Calling showInsets(%d,%b) on window that no longer"
+ " has views.", msg.arg1, msg.arg2 == 1));
}
+ clearLowProfileModeIfNeeded(msg.arg1, msg.arg2 == 1);
mInsetsController.show(msg.arg1, msg.arg2 == 1);
break;
}
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 1a90035..24538c5 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -33,7 +33,6 @@
/**
* Interface to control windows that generate insets.
*
- * TODO(118118435): Needs more information and examples once the API is more baked.
*/
public interface WindowInsetsController {
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index da26930..b4cd145 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -390,6 +390,20 @@
public static final String CHOOSER_TARGET_RANKING_ENABLED = "chooser_target_ranking_enabled";
/**
+ * (float) Weight bonus applied on top sharing shortcuts as per native ranking provided by apps.
+ * Its range need to be 0 ~ 1.
+ */
+ public static final String TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER =
+ "top_native_ranked_sharing_shortcut_booster";
+
+ /**
+ * (float) Weight bonus applied on 2nd top sharing shortcuts as per native ranking provided by
+ * apps. Its range need to be 0 ~ 1.
+ */
+ public static final String NON_TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER =
+ "non_top_native_ranked_sharing_shortcut_booster";
+
+ /**
* (boolean) Whether to enable user-drag resizing for PIP.
*/
public static final String PIP_USER_RESIZE = "pip_user_resize";
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 5a1af84..2ac7e5f 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -151,7 +151,7 @@
private static final int MAGIC = 0xBA757475; // 'BATSTATS'
// Current on-disk Parcel version
- static final int VERSION = 186 + (USE_OLD_HISTORY ? 1000 : 0);
+ static final int VERSION = 188 + (USE_OLD_HISTORY ? 1000 : 0);
// The maximum number of names wakelocks we will keep track of
// per uid; once the limit is reached, we batch the remaining wakelocks
@@ -13596,6 +13596,7 @@
mDailyStartTime = in.readLong();
mNextMinDailyDeadline = in.readLong();
mNextMaxDailyDeadline = in.readLong();
+ mBatteryTimeToFullSeconds = in.readLong();
mStartCount++;
@@ -14086,6 +14087,7 @@
out.writeLong(mDailyStartTime);
out.writeLong(mNextMinDailyDeadline);
out.writeLong(mNextMaxDailyDeadline);
+ out.writeLong(mBatteryTimeToFullSeconds);
mScreenOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
mScreenDozeTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
@@ -14669,6 +14671,7 @@
mDischargeLightDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in);
mDischargeDeepDozeCounter = new LongSamplingCounter(mOnBatteryTimeBase, in);
mLastWriteTime = in.readLong();
+ mBatteryTimeToFullSeconds = in.readLong();
mRpmStats.clear();
int NRPMS = in.readInt();
@@ -14861,6 +14864,7 @@
mDischargeLightDozeCounter.writeToParcel(out);
mDischargeDeepDozeCounter.writeToParcel(out);
out.writeLong(mLastWriteTime);
+ out.writeLong(mBatteryTimeToFullSeconds);
out.writeInt(mRpmStats.size());
for (Map.Entry<String, SamplingTimer> ent : mRpmStats.entrySet()) {
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index b12c5e9..fbbf791 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -1094,13 +1094,15 @@
mLastWindowFlags = attrs.flags;
if (insets != null) {
- final Insets systemBarInsets = insets.getInsets(WindowInsets.Type.systemBars());
final Insets stableBarInsets = insets.getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars());
- mLastTopInset = systemBarInsets.top;
- mLastBottomInset = systemBarInsets.bottom;
- mLastRightInset = systemBarInsets.right;
- mLastLeftInset = systemBarInsets.left;
+ final Insets systemInsets = Insets.min(
+ insets.getInsets(WindowInsets.Type.systemBars()
+ | WindowInsets.Type.displayCutout()), stableBarInsets);
+ mLastTopInset = systemInsets.top;
+ mLastBottomInset = systemInsets.bottom;
+ mLastRightInset = systemInsets.right;
+ mLastLeftInset = systemInsets.left;
// Don't animate if the presence of stable insets has changed, because that
// indicates that the window was either just added and received them for the
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index f11adef..7d2e32a 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -191,7 +191,7 @@
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putInt("k", 4);
FixedRotationAdjustments fixedRotationAdjustments = new FixedRotationAdjustments(
- Surface.ROTATION_90, DisplayCutout.NO_CUTOUT);
+ Surface.ROTATION_90, 1920, 1080, DisplayCutout.NO_CUTOUT);
LaunchActivityItem item = LaunchActivityItem.obtain(intent, ident, activityInfo,
config(), overrideConfig, compat, referrer, null /* voiceInteractor */,
@@ -351,7 +351,8 @@
ClientTransaction transaction = ClientTransaction.obtain(new StubAppThread(),
null /* activityToken */);
transaction.addCallback(FixedRotationAdjustmentsItem.obtain(new Binder(),
- new FixedRotationAdjustments(Surface.ROTATION_270, DisplayCutout.NO_CUTOUT)));
+ new FixedRotationAdjustments(Surface.ROTATION_270, 1920, 1080,
+ DisplayCutout.NO_CUTOUT)));
writeAndPrepareForReading(transaction);
diff --git a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
index 2fc42e9..3cf1722 100644
--- a/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
+++ b/core/tests/coretests/src/android/view/DisplayAdjustmentsTests.java
@@ -77,8 +77,10 @@
final int realRotation = Surface.ROTATION_0;
final int fixedRotation = Surface.ROTATION_90;
- mDisplayAdjustments.setFixedRotationAdjustments(
- new FixedRotationAdjustments(fixedRotation, null /* cutout */));
+ final int appWidth = 1080;
+ final int appHeight = 1920;
+ mDisplayAdjustments.setFixedRotationAdjustments(new FixedRotationAdjustments(
+ fixedRotation, appWidth, appHeight, null /* cutout */));
final int w = 1000;
final int h = 2000;
@@ -95,13 +97,21 @@
metrics.heightPixels = metrics.noncompatHeightPixels = h;
final DisplayMetrics flippedMetrics = new DisplayMetrics();
- flippedMetrics.xdpi = flippedMetrics.noncompatXdpi = h;
+ // The physical dpi should not be adjusted.
+ flippedMetrics.xdpi = flippedMetrics.noncompatXdpi = w;
flippedMetrics.widthPixels = flippedMetrics.noncompatWidthPixels = h;
- flippedMetrics.ydpi = flippedMetrics.noncompatYdpi = w;
+ flippedMetrics.ydpi = flippedMetrics.noncompatYdpi = h;
flippedMetrics.heightPixels = flippedMetrics.noncompatHeightPixels = w;
mDisplayAdjustments.adjustMetrics(metrics, realRotation);
assertEquals(flippedMetrics, metrics);
+
+ mDisplayAdjustments.adjustGlobalAppMetrics(metrics);
+
+ assertEquals(appWidth, metrics.widthPixels);
+ assertEquals(appWidth, metrics.noncompatWidthPixels);
+ assertEquals(appHeight, metrics.heightPixels);
+ assertEquals(appHeight, metrics.noncompatHeightPixels);
}
}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 5f34a5e..950666b 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -168,6 +168,7 @@
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.LOCAL_MAC_ADDRESS"/>
<permission name="android.permission.MANAGE_USERS"/>
+ <permission name="android.permission.MANAGE_SUBSCRIPTION_PLANS" />
<permission name="android.permission.MODIFY_PHONE_STATE"/>
<permission name="android.permission.PACKAGE_USAGE_STATS"/>
<permission name="android.permission.PERFORM_CDMA_PROVISIONING"/>
diff --git a/location/java/android/location/LocationManager.java b/location/java/android/location/LocationManager.java
index 6bf6034..b77a249 100644
--- a/location/java/android/location/LocationManager.java
+++ b/location/java/android/location/LocationManager.java
@@ -732,7 +732,7 @@
mContext.getAttributionTag(), transport.getListenerId());
if (cancelRemote != null) {
transport.register(mContext.getSystemService(AlarmManager.class),
- cancellationSignal);
+ cancellationSignal, cancelRemote);
if (cancellationSignal != null) {
cancellationSignal.setRemote(cancelRemote);
}
@@ -2571,7 +2571,8 @@
}
public synchronized void register(AlarmManager alarmManager,
- CancellationSignal cancellationSignal) {
+ CancellationSignal cancellationSignal,
+ ICancellationSignal remoteCancellationSignal) {
if (mConsumer == null) {
return;
}
@@ -2587,15 +2588,21 @@
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(this);
}
+
+ mRemoteCancellationSignal = remoteCancellationSignal;
}
@Override
public void onCancel() {
+ synchronized (this) {
+ mRemoteCancellationSignal = null;
+ }
remove();
}
private Consumer<Location> remove() {
Consumer<Location> consumer;
+ ICancellationSignal cancellationSignal;
synchronized (this) {
mExecutor = null;
consumer = mConsumer;
@@ -2605,6 +2612,18 @@
mAlarmManager.cancel(this);
mAlarmManager = null;
}
+
+ // ensure only one cancel event will go through
+ cancellationSignal = mRemoteCancellationSignal;
+ mRemoteCancellationSignal = null;
+ }
+
+ if (cancellationSignal != null) {
+ try {
+ cancellationSignal.cancel();
+ } catch (RemoteException e) {
+ // ignore
+ }
}
return consumer;
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
index 218c95c..ec018f9 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java
@@ -21,10 +21,13 @@
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
+import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
import androidx.annotation.VisibleForTesting;
@@ -61,7 +64,7 @@
public class CarKeyguardViewController extends OverlayViewController implements
KeyguardViewController {
private static final String TAG = "CarKeyguardViewController";
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
private final Context mContext;
private final Handler mHandler;
@@ -75,9 +78,10 @@
private final DismissCallbackRegistry mDismissCallbackRegistry;
private final ViewMediatorCallback mViewMediatorCallback;
private final CarNavigationBarController mCarNavigationBarController;
+ private final InputMethodManager mInputMethodManager;
// Needed to instantiate mBouncer.
- private final KeyguardBouncer.BouncerExpansionCallback
- mExpansionCallback = new KeyguardBouncer.BouncerExpansionCallback() {
+ private final KeyguardBouncer.BouncerExpansionCallback mExpansionCallback =
+ new KeyguardBouncer.BouncerExpansionCallback() {
@Override
public void onFullyShown() {
}
@@ -96,7 +100,8 @@
};
private final CarUserManager.UserLifecycleListener mUserLifecycleListener = (e) -> {
if (e.getEventType() == CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING) {
- revealKeyguardIfBouncerPrepared();
+ UserHandle currentUser = e.getUserHandle();
+ revealKeyguardIfBouncerPrepared(currentUser);
}
};
@@ -136,6 +141,8 @@
mDismissCallbackRegistry = dismissCallbackRegistry;
mViewMediatorCallback = viewMediatorCallback;
mCarNavigationBarController = carNavigationBarController;
+ // TODO(b/169280588): Inject InputMethodManager instead.
+ mInputMethodManager = mContext.getSystemService(InputMethodManager.class);
registerUserSwitchedListener();
}
@@ -363,9 +370,9 @@
}
/**
- * Hides Keyguard so that the transitioning Bouncer can be hidden until it is prepared. To be
- * called by {@link com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator}
- * when a new user is selected.
+ * Hides Keyguard so that the transitioning Bouncer can be hidden until it is prepared. To be
+ * called by {@link com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator}
+ * when a new user is selected.
*/
public void hideKeyguardToPrepareBouncer() {
getLayout().setVisibility(View.INVISIBLE);
@@ -376,29 +383,41 @@
mBouncer = keyguardBouncer;
}
- private void revealKeyguardIfBouncerPrepared() {
+ private void revealKeyguardIfBouncerPrepared(UserHandle currentUser) {
int reattemptDelayMillis = 50;
Runnable revealKeyguard = () -> {
if (mBouncer == null) {
if (DEBUG) {
Log.d(TAG, "revealKeyguardIfBouncerPrepared: revealKeyguard request is ignored "
- + "since the Bouncer has not been initialized yet.");
+ + "since the Bouncer has not been initialized yet.");
}
return;
}
if (!mBouncer.inTransit() || !mBouncer.isSecure()) {
getLayout().setVisibility(View.VISIBLE);
+ updateCurrentUserForPasswordEntry(currentUser);
} else {
if (DEBUG) {
Log.d(TAG, "revealKeyguardIfBouncerPrepared: Bouncer is not prepared "
+ "yet so reattempting after " + reattemptDelayMillis + "ms.");
}
- mHandler.postDelayed(this::revealKeyguardIfBouncerPrepared, reattemptDelayMillis);
+ mHandler.postDelayed(() -> revealKeyguardIfBouncerPrepared(currentUser),
+ reattemptDelayMillis);
}
};
mHandler.post(revealKeyguard);
}
+ private void updateCurrentUserForPasswordEntry(UserHandle currentUser) {
+ EditText passwordEntry = getLayout().findViewById(R.id.passwordEntry);
+ if (passwordEntry != null) {
+ mHandler.post(() -> {
+ mInputMethodManager.restartInput(passwordEntry);
+ passwordEntry.setTextOperationUser(currentUser);
+ });
+ }
+ }
+
private void notifyKeyguardUpdateMonitor() {
mKeyguardUpdateMonitor.onKeyguardVisibilityChanged(mShowing);
if (mBouncer != null) {
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
index e72df6b..2efa2b3d 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/SystemBarConfigs.java
@@ -96,9 +96,11 @@
populateMaps();
readConfigs();
+
checkEnabledBarsHaveUniqueBarTypes();
checkSystemBarEnabledForNotificationPanel();
checkHideBottomBarForKeyboardConfigSync();
+
setInsetPaddingsForOverlappingCorners();
sortSystemBarSidesByZOrder();
}
@@ -153,10 +155,10 @@
BAR_TITLE_MAP.put(LEFT, "LeftCarSystemBar");
BAR_TITLE_MAP.put(RIGHT, "RightCarSystemBar");
- BAR_GESTURE_MAP.put(TOP, InsetsState.ITYPE_TOP_GESTURES);
- BAR_GESTURE_MAP.put(BOTTOM, InsetsState.ITYPE_BOTTOM_GESTURES);
- BAR_GESTURE_MAP.put(LEFT, InsetsState.ITYPE_LEFT_GESTURES);
- BAR_GESTURE_MAP.put(RIGHT, InsetsState.ITYPE_RIGHT_GESTURES);
+ BAR_GESTURE_MAP.put(TOP, InsetsState.ITYPE_TOP_MANDATORY_GESTURES);
+ BAR_GESTURE_MAP.put(BOTTOM, InsetsState.ITYPE_BOTTOM_MANDATORY_GESTURES);
+ BAR_GESTURE_MAP.put(LEFT, InsetsState.ITYPE_LEFT_MANDATORY_GESTURES);
+ BAR_GESTURE_MAP.put(RIGHT, InsetsState.ITYPE_RIGHT_MANDATORY_GESTURES);
}
private void readConfigs() {
diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml
index ed870f8..170f2c4 100644
--- a/packages/SystemUI/res/layout/media_view.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -166,8 +166,7 @@
android:layout_height="wrap_content"
android:clickable="true"
android:maxHeight="@dimen/qs_media_enabled_seekbar_height"
- android:paddingTop="16dp"
- android:paddingBottom="16dp"
+ android:paddingVertical="@dimen/qs_media_enabled_seekbar_vertical_padding"
android:thumbTint="@color/media_primary_text"
android:progressTint="@color/media_seekbar_progress"
android:progressBackgroundTint="@color/media_disabled"
diff --git a/packages/SystemUI/res/values-h740dp-port/dimens.xml b/packages/SystemUI/res/values-h740dp-port/dimens.xml
new file mode 100644
index 0000000..966066f
--- /dev/null
+++ b/packages/SystemUI/res/values-h740dp-port/dimens.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2020 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
+ -->
+
+<resources>
+ <dimen name="qs_tile_height">106dp</dimen>
+ <dimen name="qs_tile_margin_vertical">24dp</dimen>
+
+ <!-- The height of the qs customize header. Should be
+ (qs_panel_padding_top (48dp) + brightness_mirror_height (48dp) + qs_tile_margin_top (18dp)) -
+ (Toolbar_minWidth (56dp) + qs_tile_margin_top_bottom (12dp))
+ -->
+ <dimen name="qs_customize_header_min_height">46dp</dimen>
+ <dimen name="qs_tile_margin_top">18dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 5b7bee6..f07627a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -482,20 +482,21 @@
<!-- The size of the gesture span needed to activate the "pull" notification expansion -->
<dimen name="pull_span_min">25dp</dimen>
- <dimen name="qs_tile_height">106dp</dimen>
+ <dimen name="qs_tile_height">96dp</dimen>
<!--notification_side_paddings + notification_content_margin_start - (qs_quick_tile_size - qs_tile_background_size) / 2 -->
<dimen name="qs_tile_layout_margin_side">18dp</dimen>
<dimen name="qs_tile_margin_horizontal">18dp</dimen>
<dimen name="qs_tile_margin_horizontal_two_line">2dp</dimen>
- <dimen name="qs_tile_margin_vertical">24dp</dimen>
+ <dimen name="qs_tile_margin_vertical">2dp</dimen>
<dimen name="qs_tile_margin_top_bottom">12dp</dimen>
<dimen name="qs_tile_margin_top_bottom_negative">-12dp</dimen>
<!-- The height of the qs customize header. Should be
- (qs_panel_padding_top (48dp) + brightness_mirror_height (48dp) + qs_tile_margin_top (18dp)) -
+ (qs_panel_padding_top (48dp) + brightness_mirror_height (48dp) + qs_tile_margin_top (0dp)) -
(Toolbar_minWidth (56dp) + qs_tile_margin_top_bottom (12dp))
-->
- <dimen name="qs_customize_header_min_height">46dp</dimen>
- <dimen name="qs_tile_margin_top">18dp</dimen>
+ <dimen name="qs_customize_header_min_height">28dp</dimen>
+ <dimen name="qs_tile_margin_top">0dp</dimen>
+ <dimen name="qs_tile_icon_background_stroke_width">-1dp</dimen>
<dimen name="qs_tile_background_size">44dp</dimen>
<dimen name="qs_quick_tile_size">48dp</dimen>
<dimen name="qs_quick_tile_padding">12dp</dimen>
@@ -1295,6 +1296,8 @@
<dimen name="qs_footer_horizontal_margin">22dp</dimen>
<dimen name="qs_media_disabled_seekbar_height">1dp</dimen>
<dimen name="qs_media_enabled_seekbar_height">3dp</dimen>
+ <dimen name="qs_media_enabled_seekbar_vertical_padding">15dp</dimen>
+ <dimen name="qs_media_disabled_seekbar_vertical_padding">16dp</dimen>
<dimen name="magnification_border_size">5dp</dimen>
<dimen name="magnification_frame_move_short">5dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index a56f6f5..b8e8db5 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -177,6 +177,9 @@
<item type="id" name="accessibility_action_controls_move_before" />
<item type="id" name="accessibility_action_controls_move_after" />
+ <item type="id" name="accessibility_action_qs_move_to_position" />
+ <item type="id" name="accessibility_action_qs_add_to_position" />
+
<!-- Accessibility actions for PIP -->
<item type="id" name="action_pip_resize" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 38501eb..77ce39f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2267,23 +2267,26 @@
<!-- Accessibility action for moving docked stack divider to make the bottom screen full screen [CHAR LIMIT=NONE] -->
<string name="accessibility_action_divider_bottom_full">Bottom full screen</string>
- <!-- Accessibility description of a QS tile while editing positions [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_label">Position <xliff:g id="position" example="2">%1$d</xliff:g>, <xliff:g id="tile_name" example="Wi-Fi">%2$s</xliff:g>. Double tap to edit.</string>
+ <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_remove_tile_action">remove tile</string>
- <!-- Accessibility description of a QS tile while editing positions [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_add_tile_label"><xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g>. Double tap to add.</string>
+ <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to end" in screen readers [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_add_action">add tile to end</string>
- <!-- Accessibility description of option to move QS tile [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_move_tile">Move <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g></string>
+ <!-- Accessibility action for context menu to move QS tile [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_start_move">Move tile</string>
- <!-- Accessibility description of option to remove QS tile [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_remove_tile">Remove <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g></string>
+ <!-- Accessibility action for context menu to add QS tile [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_start_add">Add tile</string>
- <!-- Accessibility action when QS tile is to be added [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_add">Add <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g> to position <xliff:g id="position" example="5">%2$d</xliff:g></string>
+ <!-- Accessibility description when QS tile is to be moved, indicating the destination position [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_move_to_position">Move to <xliff:g id="position" example="5">%1$d</xliff:g></string>
- <!-- Accessibility action when QS tile is to be moved [CHAR LIMIT=NONE] -->
- <string name="accessibility_qs_edit_tile_move">Move <xliff:g id="tile_name" example="Wi-Fi">%1$s</xliff:g> to position <xliff:g id="position" example="5">%2$d</xliff:g></string>
+ <!-- Accessibility description when QS tile is to be added, indicating the destination position [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_tile_add_to_position">Add to position <xliff:g id="position" example="5">%1$d</xliff:g></string>
+
+ <!-- Accessibility description indicating the currently selected tile's position. Only used for tiles that are currently in use [CHAR LIMIT=NONE] -->
+ <string name="accessibility_qs_edit_position">Position <xliff:g id="position" example="5">%1$d</xliff:g></string>
<!-- Accessibility label for window when QS editing is happening [CHAR LIMIT=NONE] -->
<string name="accessibility_desc_quick_settings_edit">Quick settings editor.</string>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index c6d1286..b71e3ad 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -286,6 +286,15 @@
}
/**
+ * Sets whether this bubble is considered visually interruptive. Normally pulled from the
+ * {@link NotificationEntry}, this method is purely for testing.
+ */
+ @VisibleForTesting
+ void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) {
+ mIsVisuallyInterruptive = visuallyInterruptive;
+ }
+
+ /**
* Starts a task to inflate & load any necessary information to display a bubble.
*
* @param callback the callback to notify one the bubble is ready to be displayed.
@@ -411,6 +420,7 @@
} else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) {
// Was an intent bubble now it's a shortcut bubble... still unregister the listener
mIntent.unregisterCancelListener(mIntentCancelListener);
+ mIntentActive = false;
mIntent = null;
}
mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 2948b47..5deae92 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -82,7 +82,6 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.Dumpable;
-import com.android.systemui.bubbles.animation.StackAnimationController;
import com.android.systemui.bubbles.dagger.BubbleModule;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
@@ -407,7 +406,8 @@
if (bubble.getBubbleIntent() == null) {
return;
}
- if (bubble.isIntentActive()) {
+ if (bubble.isIntentActive()
+ || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
bubble.setPendingIntentCanceled();
return;
}
@@ -1120,8 +1120,17 @@
if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
notif.setInterruption();
}
- Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
- inflateAndAdd(bubble, suppressFlyout, showInShade);
+ if (!notif.getRanking().visuallyInterruptive()
+ && (notif.getBubbleMetadata() != null
+ && !notif.getBubbleMetadata().getAutoExpandBubble())
+ && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) {
+ // Update the bubble but don't promote it out of overflow
+ Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey());
+ b.setEntry(notif);
+ } else {
+ Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */);
+ inflateAndAdd(bubble, suppressFlyout, showInShade);
+ }
}
void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index d2dc506..85ea8bc 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -277,7 +277,8 @@
} else {
// Updates an existing bubble
bubble.setSuppressFlyout(suppressFlyout);
- doUpdate(bubble);
+ // If there is no flyout, we probably shouldn't show the bubble at the top
+ doUpdate(bubble, !suppressFlyout /* reorder */);
}
if (bubble.shouldAutoExpand()) {
@@ -431,12 +432,12 @@
}
}
- private void doUpdate(Bubble bubble) {
+ private void doUpdate(Bubble bubble, boolean reorder) {
if (DEBUG_BUBBLE_DATA) {
Log.d(TAG, "doUpdate: " + bubble);
}
mStateChange.updatedBubble = bubble;
- if (!isExpanded()) {
+ if (!isExpanded() && reorder) {
int prevPos = mBubbles.indexOf(bubble);
mBubbles.remove(bubble);
mBubbles.add(0, bubble);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
new file mode 100644
index 0000000..aca033e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+import android.os.Bundle;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link MediaBrowser} constructor
+ */
+public class MediaBrowserFactory {
+ private final Context mContext;
+
+ @Inject
+ public MediaBrowserFactory(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Creates a new MediaBrowser
+ *
+ * @param serviceComponent
+ * @param callback
+ * @param rootHints
+ * @return
+ */
+ public MediaBrowser create(ComponentName serviceComponent,
+ MediaBrowser.ConnectionCallback callback, Bundle rootHints) {
+ return new MediaBrowser(mContext, serviceComponent, callback, rootHints);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 3aa37a2..d8d9bd7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -174,7 +174,7 @@
mediaManager.addListener(object : MediaDataManager.Listener {
override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
addOrUpdatePlayer(key, oldKey, data)
- val canRemove = data.isPlaying?.let { !it } ?: data.isClearable
+ val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active
if (canRemove && !Utils.useMediaResumption(context)) {
// This view isn't playing, let's remove this! This happens e.g when
// dismissing/timing out a view. We still have the data around because
@@ -250,13 +250,13 @@
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
newPlayer.view?.player?.setLayoutParams(lp)
- newPlayer.bind(data)
+ newPlayer.bind(data, key)
newPlayer.setListening(currentlyExpanded)
MediaPlayerData.addMediaPlayer(key, data, newPlayer)
updatePlayerToState(newPlayer, noAnimation = true)
reorderAllPlayers()
} else {
- existingPlayer.bind(data)
+ existingPlayer.bind(data, key)
MediaPlayerData.addMediaPlayer(key, data, existingPlayer)
if (visualStabilityManager.isReorderingAllowed) {
reorderAllPlayers()
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index e55678dc..810cecc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -82,6 +82,7 @@
private Context mContext;
private PlayerViewHolder mViewHolder;
+ private String mKey;
private MediaViewController mMediaViewController;
private MediaSession.Token mToken;
private MediaController mController;
@@ -206,10 +207,11 @@
/**
* Bind this view based on the data given
*/
- public void bind(@NonNull MediaData data) {
+ public void bind(@NonNull MediaData data, String key) {
if (mViewHolder == null) {
return;
}
+ mKey = key;
MediaSession.Token token = data.getToken();
mBackgroundColor = data.getBackgroundColor();
if (mToken == null || !mToken.equals(token)) {
@@ -359,10 +361,10 @@
// Dismiss
mViewHolder.getDismiss().setOnClickListener(v -> {
- if (data.getNotificationKey() != null) {
+ if (mKey != null) {
closeGuts();
mKeyguardDismissUtil.executeWhenUnlocked(() -> {
- mMediaDataManagerLazy.get().dismissMediaData(data.getNotificationKey(),
+ mMediaDataManagerLazy.get().dismissMediaData(mKey,
MediaViewController.GUTS_ANIMATION_DURATION + 100);
return true;
}, /* requiresShadeOpen */ true);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
index 1ac3034..936db87 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -28,6 +28,7 @@
import android.provider.Settings
import android.service.media.MediaBrowserService
import android.util.Log
+import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.tuner.TunerService
@@ -47,7 +48,8 @@
private val context: Context,
private val broadcastDispatcher: BroadcastDispatcher,
@Background private val backgroundExecutor: Executor,
- private val tunerService: TunerService
+ private val tunerService: TunerService,
+ private val mediaBrowserFactory: ResumeMediaBrowserFactory
) : MediaDataManager.Listener {
private var useMediaResumption: Boolean = Utils.useMediaResumption(context)
@@ -58,7 +60,8 @@
private var mediaBrowser: ResumeMediaBrowser? = null
private var currentUserId: Int = context.userId
- private val userChangeReceiver = object : BroadcastReceiver() {
+ @VisibleForTesting
+ val userChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_USER_UNLOCKED == intent.action) {
loadMediaResumptionControls()
@@ -142,7 +145,7 @@
}
resumeComponents.forEach {
- val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it)
+ val browser = mediaBrowserFactory.create(mediaBrowserCallback, it)
browser.findRecentMedia()
}
}
@@ -181,14 +184,10 @@
private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
Log.d(TAG, "Testing if we can connect to $componentName")
mediaBrowser?.disconnect()
- mediaBrowser = ResumeMediaBrowser(context,
+ mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
- Log.d(TAG, "yes we can resume with $componentName")
- mediaDataManager.setResumeAction(key, getResumeAction(componentName))
- updateResumptionList(componentName)
- mediaBrowser?.disconnect()
- mediaBrowser = null
+ Log.d(TAG, "Connected to $componentName")
}
override fun onError() {
@@ -197,6 +196,19 @@
mediaBrowser?.disconnect()
mediaBrowser = null
}
+
+ override fun addTrack(
+ desc: MediaDescription,
+ component: ComponentName,
+ browser: ResumeMediaBrowser
+ ) {
+ // Since this is a test, just save the component for later
+ Log.d(TAG, "Can get resumable media from $componentName")
+ mediaDataManager.setResumeAction(key, getResumeAction(componentName))
+ updateResumptionList(componentName)
+ mediaBrowser?.disconnect()
+ mediaBrowser = null
+ }
},
componentName)
mediaBrowser?.testConnection()
@@ -233,7 +245,7 @@
private fun getResumeAction(componentName: ComponentName): Runnable {
return Runnable {
mediaBrowser?.disconnect()
- mediaBrowser = ResumeMediaBrowser(context,
+ mediaBrowser = mediaBrowserFactory.create(
object : ResumeMediaBrowser.Callback() {
override fun onConnected() {
if (mediaBrowser?.token == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
index 8662aac..dcb7767 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
@@ -54,32 +54,35 @@
if (mediaListeners.containsKey(key)) {
return
}
- // Having an old key means that we're migrating from/to resumption. We should invalidate
- // the old listener and create a new one.
+ // Having an old key means that we're migrating from/to resumption. We should update
+ // the old listener to make sure that events will be dispatched to the new location.
val migrating = oldKey != null && key != oldKey
var wasPlaying = false
if (migrating) {
- if (mediaListeners.containsKey(oldKey)) {
- val oldListener = mediaListeners.remove(oldKey)
- wasPlaying = oldListener?.playing ?: false
- oldListener?.destroy()
+ val reusedListener = mediaListeners.remove(oldKey)
+ if (reusedListener != null) {
+ wasPlaying = reusedListener.playing ?: false
if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption")
+ reusedListener.mediaData = data
+ reusedListener.key = key
+ mediaListeners[key] = reusedListener
+ if (wasPlaying != reusedListener.playing) {
+ // If a player becomes active because of a migration, we'll need to broadcast
+ // its state. Doing it now would lead to reentrant callbacks, so let's wait
+ // until we're done.
+ mainExecutor.execute {
+ if (mediaListeners[key]?.playing == true) {
+ if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key")
+ timeoutCallback.invoke(key, false /* timedOut */)
+ }
+ }
+ }
+ return
} else {
Log.w(TAG, "Old key $oldKey for player $key doesn't exist. Continuing...")
}
}
mediaListeners[key] = PlaybackStateListener(key, data)
-
- // If a player becomes active because of a migration, we'll need to broadcast its state.
- // Doing it now would lead to reentrant callbacks, so let's wait until we're done.
- if (migrating && mediaListeners[key]?.playing != wasPlaying) {
- mainExecutor.execute {
- if (mediaListeners[key]?.playing == true) {
- if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key")
- timeoutCallback.invoke(key, false /* timedOut */)
- }
- }
- }
}
override fun onMediaDataRemoved(key: String) {
@@ -91,30 +94,39 @@
}
private inner class PlaybackStateListener(
- private val key: String,
+ var key: String,
data: MediaData
) : MediaController.Callback() {
var timedOut = false
var playing: Boolean? = null
+ var mediaData: MediaData = data
+ set(value) {
+ mediaController?.unregisterCallback(this)
+ field = value
+ mediaController = if (field.token != null) {
+ mediaControllerFactory.create(field.token)
+ } else {
+ null
+ }
+ mediaController?.registerCallback(this)
+ // Let's register the cancellations, but not dispatch events now.
+ // Timeouts didn't happen yet and reentrant events are troublesome.
+ processState(mediaController?.playbackState, dispatchEvents = false)
+ }
+
// Resume controls may have null token
- private val mediaController = if (data.token != null) {
- mediaControllerFactory.create(data.token)
- } else {
- null
- }
+ private var mediaController: MediaController? = null
private var cancellation: Runnable? = null
init {
- mediaController?.registerCallback(this)
- // Let's register the cancellations, but not dispatch events now.
- // Timeouts didn't happen yet and reentrant events are troublesome.
- processState(mediaController?.playbackState, dispatchEvents = false)
+ mediaData = data
}
fun destroy() {
mediaController?.unregisterCallback(this)
+ cancellation?.run()
}
override fun onPlaybackStateChanged(state: PlaybackState?) {
@@ -171,4 +183,4 @@
cancellation = null
}
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
index 68b6785..a4d4436 100644
--- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
@@ -30,6 +30,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.List;
/**
@@ -46,6 +48,7 @@
private static final String TAG = "ResumeMediaBrowser";
private final Context mContext;
private final Callback mCallback;
+ private MediaBrowserFactory mBrowserFactory;
private MediaBrowser mMediaBrowser;
private ComponentName mComponentName;
@@ -55,10 +58,12 @@
* @param callback used to report media items found
* @param componentName Component name of the MediaBrowserService this browser will connect to
*/
- public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+ public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName,
+ MediaBrowserFactory browserFactory) {
mContext = context;
mCallback = callback;
mComponentName = componentName;
+ mBrowserFactory = browserFactory;
}
/**
@@ -74,7 +79,7 @@
disconnect();
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext,
+ mMediaBrowser = mBrowserFactory.create(
mComponentName,
mConnectionCallback,
rootHints);
@@ -88,17 +93,19 @@
List<MediaBrowser.MediaItem> children) {
if (children.size() == 0) {
Log.d(TAG, "No children found for " + mComponentName);
- return;
- }
- // We ask apps to return a playable item as the first child when sending
- // a request with EXTRA_RECENT; if they don't, no resume controls
- MediaBrowser.MediaItem child = children.get(0);
- MediaDescription desc = child.getDescription();
- if (child.isPlayable() && mMediaBrowser != null) {
- mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
- ResumeMediaBrowser.this);
+ mCallback.onError();
} else {
- Log.d(TAG, "Child found but not playable for " + mComponentName);
+ // We ask apps to return a playable item as the first child when sending
+ // a request with EXTRA_RECENT; if they don't, no resume controls
+ MediaBrowser.MediaItem child = children.get(0);
+ MediaDescription desc = child.getDescription();
+ if (child.isPlayable() && mMediaBrowser != null) {
+ mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
+ ResumeMediaBrowser.this);
+ } else {
+ Log.d(TAG, "Child found but not playable for " + mComponentName);
+ mCallback.onError();
+ }
}
disconnect();
}
@@ -131,7 +138,7 @@
Log.d(TAG, "Service connected for " + mComponentName);
if (mMediaBrowser != null && mMediaBrowser.isConnected()) {
String root = mMediaBrowser.getRoot();
- if (!TextUtils.isEmpty(root)) {
+ if (!TextUtils.isEmpty(root) && mMediaBrowser != null) {
mCallback.onConnected();
mMediaBrowser.subscribe(root, mSubscriptionCallback);
return;
@@ -182,7 +189,7 @@
disconnect();
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext, mComponentName,
+ mMediaBrowser = mBrowserFactory.create(mComponentName,
new MediaBrowser.ConnectionCallback() {
@Override
public void onConnected() {
@@ -192,7 +199,7 @@
return;
}
MediaSession.Token token = mMediaBrowser.getSessionToken();
- MediaController controller = new MediaController(mContext, token);
+ MediaController controller = createMediaController(token);
controller.getTransportControls();
controller.getTransportControls().prepare();
controller.getTransportControls().play();
@@ -212,6 +219,11 @@
mMediaBrowser.connect();
}
+ @VisibleForTesting
+ protected MediaController createMediaController(MediaSession.Token token) {
+ return new MediaController(mContext, token);
+ }
+
/**
* Get the media session token
* @return the token, or null if the MediaBrowser is null or disconnected
@@ -235,42 +247,19 @@
/**
* Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
- * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
- * depending on whether it was successful.
+ * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is
+ * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more
+ * detailed logging if the service has issues. If it cannot connect, or cannot find valid media,
+ * then ResumeMediaBrowser.Callback#onError will be called.
* ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
*/
public void testConnection() {
disconnect();
- final MediaBrowser.ConnectionCallback connectionCallback =
- new MediaBrowser.ConnectionCallback() {
- @Override
- public void onConnected() {
- Log.d(TAG, "connected");
- if (mMediaBrowser == null || !mMediaBrowser.isConnected()
- || TextUtils.isEmpty(mMediaBrowser.getRoot())) {
- mCallback.onError();
- } else {
- mCallback.onConnected();
- }
- }
-
- @Override
- public void onConnectionSuspended() {
- Log.d(TAG, "suspended");
- mCallback.onError();
- }
-
- @Override
- public void onConnectionFailed() {
- Log.d(TAG, "failed");
- mCallback.onError();
- }
- };
Bundle rootHints = new Bundle();
rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
- mMediaBrowser = new MediaBrowser(mContext,
+ mMediaBrowser = mBrowserFactory.create(
mComponentName,
- connectionCallback,
+ mConnectionCallback,
rootHints);
mMediaBrowser.connect();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
new file mode 100644
index 0000000..2261aa5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import javax.inject.Inject;
+
+/**
+ * Testable wrapper around {@link ResumeMediaBrowser} constructor
+ */
+public class ResumeMediaBrowserFactory {
+ private final Context mContext;
+ private final MediaBrowserFactory mBrowserFactory;
+
+ @Inject
+ public ResumeMediaBrowserFactory(Context context, MediaBrowserFactory browserFactory) {
+ mContext = context;
+ mBrowserFactory = browserFactory;
+ }
+
+ /**
+ * Creates a new ResumeMediaBrowser.
+ *
+ * @param callback will be called on connection or error, and addTrack when media item found
+ * @param componentName component to browse
+ * @return
+ */
+ public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback,
+ ComponentName componentName) {
+ return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
index 1ae54d6..d789501 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
@@ -28,10 +28,14 @@
*/
class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarViewModel.Progress> {
- val seekBarDefaultMaxHeight = holder.seekBar.context.resources
+ val seekBarEnabledMaxHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height)
val seekBarDisabledHeight = holder.seekBar.context.resources
.getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height)
+ val seekBarEnabledVerticalPadding = holder.seekBar.context.resources
+ .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_vertical_padding)
+ val seekBarDisabledVerticalPadding = holder.seekBar.context.resources
+ .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_vertical_padding)
/** Updates seek bar views when the data model changes. */
@UiThread
@@ -39,6 +43,7 @@
if (!data.enabled) {
if (holder.seekBar.maxHeight != seekBarDisabledHeight) {
holder.seekBar.maxHeight = seekBarDisabledHeight
+ setVerticalPadding(seekBarDisabledVerticalPadding)
}
holder.seekBar.setEnabled(false)
holder.seekBar.getThumb().setAlpha(0)
@@ -51,8 +56,9 @@
holder.seekBar.getThumb().setAlpha(if (data.seekAvailable) 255 else 0)
holder.seekBar.setEnabled(data.seekAvailable)
- if (holder.seekBar.maxHeight != seekBarDefaultMaxHeight) {
- holder.seekBar.maxHeight = seekBarDefaultMaxHeight
+ if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) {
+ holder.seekBar.maxHeight = seekBarEnabledMaxHeight
+ setVerticalPadding(seekBarEnabledVerticalPadding)
}
data.duration?.let {
@@ -67,4 +73,11 @@
it / DateUtils.SECOND_IN_MILLIS))
}
}
+
+ @UiThread
+ fun setVerticalPadding(padding: Int) {
+ val leftPadding = holder.seekBar.paddingLeft
+ val rightPadding = holder.seekBar.paddingRight
+ holder.seekBar.setPadding(leftPadding, padding, rightPadding, padding)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
index 2a83aa0..586399c 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -562,8 +562,10 @@
// TODO: Check if the action drawable has changed before we reload it
action.getIcon().loadDrawableAsync(this, d -> {
- d.setTint(Color.WHITE);
- actionView.setImageDrawable(d);
+ if (d != null) {
+ d.setTint(Color.WHITE);
+ actionView.setImageDrawable(d);
+ }
}, mHandler);
actionView.setContentDescription(action.getContentDescription());
if (action.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 7cd1c78..44803ae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -177,6 +177,16 @@
}
@Override
+ public void endFakeDrag() {
+ try {
+ super.endFakeDrag();
+ } catch (NullPointerException e) {
+ // Not sure what's going on. Let's log it
+ Log.e(TAG, "endFakeDrag called without velocityTracker", e);
+ }
+ }
+
+ @Override
public void computeScroll() {
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
if (!isFakeDragging()) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index e738cec..bffeb3e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -14,11 +14,8 @@
package com.android.systemui.qs.customize;
-import android.app.AlertDialog;
-import android.app.AlertDialog.Builder;
import android.content.ComponentName;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
@@ -28,10 +25,11 @@
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
import androidx.recyclerview.widget.ItemTouchHelper;
@@ -49,7 +47,6 @@
import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.tileimpl.QSIconViewImpl;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.ArrayList;
import java.util.List;
@@ -78,10 +75,10 @@
private final List<TileInfo> mTiles = new ArrayList<>();
private final ItemTouchHelper mItemTouchHelper;
private final ItemDecoration mDecoration;
- private final AccessibilityManager mAccessibilityManager;
private final int mMinNumTiles;
private int mEditIndex;
private int mTileDividerIndex;
+ private int mFocusIndex;
private boolean mNeedsFocus;
private List<String> mCurrentSpecs;
private List<TileInfo> mOtherTiles;
@@ -90,17 +87,28 @@
private Holder mCurrentDrag;
private int mAccessibilityAction = ACTION_NONE;
private int mAccessibilityFromIndex;
- private CharSequence mAccessibilityFromLabel;
private QSTileHost mHost;
private final UiEventLogger mUiEventLogger;
+ private final AccessibilityDelegateCompat mAccessibilityDelegate;
+ private RecyclerView mRecyclerView;
public TileAdapter(Context context, UiEventLogger uiEventLogger) {
mContext = context;
mUiEventLogger = uiEventLogger;
- mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mItemTouchHelper = new ItemTouchHelper(mCallbacks);
mDecoration = new TileItemDecoration(context);
mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles);
+ mAccessibilityDelegate = new TileAdapterDelegate();
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
}
public void setHost(QSTileHost host) {
@@ -130,7 +138,6 @@
// Remove blank tile from last spot
mTiles.remove(--mEditIndex);
// Update the tile divider position
- mTileDividerIndex--;
notifyDataSetChanged();
}
mAccessibilityAction = ACTION_NONE;
@@ -241,14 +248,12 @@
}
private void setSelectableForHeaders(View view) {
- if (mAccessibilityManager.isTouchExplorationEnabled()) {
- final boolean selectable = mAccessibilityAction == ACTION_NONE;
- view.setFocusable(selectable);
- view.setImportantForAccessibility(selectable
- ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
- : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- view.setFocusableInTouchMode(selectable);
- }
+ final boolean selectable = mAccessibilityAction == ACTION_NONE;
+ view.setFocusable(selectable);
+ view.setImportantForAccessibility(selectable
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ view.setFocusableInTouchMode(selectable);
}
@Override
@@ -285,12 +290,11 @@
holder.mTileView.setVisibility(View.VISIBLE);
holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
holder.mTileView.setContentDescription(mContext.getString(
- R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel,
- position));
+ R.string.accessibility_qs_edit_tile_add_to_position, position));
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- selectPosition(holder.getAdapterPosition(), v);
+ selectPosition(holder.getLayoutPosition());
}
});
focusOnHolder(holder);
@@ -299,54 +303,49 @@
TileInfo info = mTiles.get(position);
- if (position > mEditIndex) {
+ final boolean selectable = 0 < position && position < mEditIndex;
+ if (selectable && mAccessibilityAction == ACTION_ADD) {
info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_add_tile_label, info.state.label);
- } else if (mAccessibilityAction == ACTION_ADD) {
+ R.string.accessibility_qs_edit_tile_add_to_position, position);
+ } else if (selectable && mAccessibilityAction == ACTION_MOVE) {
info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel, position);
- } else if (mAccessibilityAction == ACTION_MOVE) {
- info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_move, mAccessibilityFromLabel, position);
+ R.string.accessibility_qs_edit_tile_move_to_position, position);
} else {
- info.state.contentDescription = mContext.getString(
- R.string.accessibility_qs_edit_tile_label, position, info.state.label);
+ info.state.contentDescription = info.state.label;
}
+ info.state.expandedAccessibilityClassName = "";
+
holder.mTileView.handleStateChanged(info.state);
holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem);
+ holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ holder.mTileView.setClickable(true);
+ holder.mTileView.setOnClickListener(null);
+ holder.mTileView.setFocusable(true);
+ holder.mTileView.setFocusableInTouchMode(true);
- if (mAccessibilityManager.isTouchExplorationEnabled()) {
- final boolean selectable = mAccessibilityAction == ACTION_NONE || position < mEditIndex;
+ if (mAccessibilityAction != ACTION_NONE) {
holder.mTileView.setClickable(selectable);
holder.mTileView.setFocusable(selectable);
+ holder.mTileView.setFocusableInTouchMode(selectable);
holder.mTileView.setImportantForAccessibility(selectable
? View.IMPORTANT_FOR_ACCESSIBILITY_YES
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
- holder.mTileView.setFocusableInTouchMode(selectable);
if (selectable) {
holder.mTileView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- int position = holder.getAdapterPosition();
+ int position = holder.getLayoutPosition();
if (position == RecyclerView.NO_POSITION) return;
if (mAccessibilityAction != ACTION_NONE) {
- selectPosition(position, v);
- } else {
- if (position < mEditIndex && canRemoveTiles()) {
- showAccessibilityDialog(position, v);
- } else if (position < mEditIndex && !canRemoveTiles()) {
- startAccessibleMove(position);
- } else {
- startAccessibleAdd(position);
- }
+ selectPosition(position);
}
}
});
- if (position == mAccessibilityFromIndex) {
- focusOnHolder(holder);
- }
}
}
+ if (position == mFocusIndex) {
+ focusOnHolder(holder);
+ }
}
private void focusOnHolder(Holder holder) {
@@ -360,9 +359,13 @@
int oldLeft, int oldTop, int oldRight, int oldBottom) {
holder.mTileView.removeOnLayoutChangeListener(this);
holder.mTileView.requestFocus();
+ if (mAccessibilityAction == ACTION_NONE) {
+ holder.mTileView.clearFocus();
+ }
}
});
mNeedsFocus = false;
+ mFocusIndex = RecyclerView.NO_POSITION;
}
}
@@ -370,72 +373,77 @@
return mCurrentSpecs.size() > mMinNumTiles;
}
- private void selectPosition(int position, View v) {
+ private void selectPosition(int position) {
if (mAccessibilityAction == ACTION_ADD) {
// Remove the placeholder.
mTiles.remove(mEditIndex--);
- notifyItemRemoved(mEditIndex);
}
mAccessibilityAction = ACTION_NONE;
- move(mAccessibilityFromIndex, position, v);
+ move(mAccessibilityFromIndex, position, false);
+ mFocusIndex = position;
+ mNeedsFocus = true;
notifyDataSetChanged();
}
- private void showAccessibilityDialog(final int position, final View v) {
- final TileInfo info = mTiles.get(position);
- CharSequence[] options = new CharSequence[] {
- mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label),
- mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label),
- };
- AlertDialog dialog = new Builder(mContext)
- .setItems(options, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (which == 0) {
- startAccessibleMove(position);
- } else {
- move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v);
- notifyItemChanged(mTileDividerIndex);
- notifyDataSetChanged();
- }
- }
- }).setNegativeButton(android.R.string.cancel, null)
- .create();
- SystemUIDialog.setShowForAllUsers(dialog, true);
- SystemUIDialog.applyFlags(dialog);
- dialog.show();
- }
-
private void startAccessibleAdd(int position) {
mAccessibilityFromIndex = position;
- mAccessibilityFromLabel = mTiles.get(position).state.label;
mAccessibilityAction = ACTION_ADD;
// Add placeholder for last slot.
mTiles.add(mEditIndex++, null);
// Update the tile divider position
mTileDividerIndex++;
+ mFocusIndex = mEditIndex - 1;
mNeedsFocus = true;
+ if (mRecyclerView != null) {
+ mRecyclerView.post(() -> mRecyclerView.smoothScrollToPosition(mFocusIndex));
+ }
notifyDataSetChanged();
}
private void startAccessibleMove(int position) {
mAccessibilityFromIndex = position;
- mAccessibilityFromLabel = mTiles.get(position).state.label;
mAccessibilityAction = ACTION_MOVE;
+ mFocusIndex = position;
mNeedsFocus = true;
notifyDataSetChanged();
}
+ private boolean canRemoveFromPosition(int position) {
+ return canRemoveTiles() && isCurrentTile(position);
+ }
+
+ private boolean isCurrentTile(int position) {
+ return position < mEditIndex;
+ }
+
+ private boolean canAddFromPosition(int position) {
+ return position > mEditIndex;
+ }
+
+ private void addFromPosition(int position) {
+ if (!canAddFromPosition(position)) return;
+ move(position, mEditIndex);
+ }
+
+ private void removeFromPosition(int position) {
+ if (!canRemoveFromPosition(position)) return;
+ TileInfo info = mTiles.get(position);
+ move(position, info.isSystem ? mEditIndex : mTileDividerIndex);
+ }
+
public SpanSizeLookup getSizeLookup() {
return mSizeLookup;
}
- private boolean move(int from, int to, View v) {
+ private boolean move(int from, int to) {
+ return move(from, to, true);
+ }
+
+ private boolean move(int from, int to, boolean notify) {
if (to == from) {
return true;
}
- CharSequence fromLabel = mTiles.get(from).state.label;
- move(from, to, mTiles);
+ move(from, to, mTiles, notify);
updateDividerLocations();
if (to >= mEditIndex) {
mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to)));
@@ -477,9 +485,11 @@
return spec;
}
- private <T> void move(int from, int to, List<T> list) {
+ private <T> void move(int from, int to, List<T> list, boolean notify) {
list.add(to, list.remove(from));
- notifyItemMoved(from, to);
+ if (notify) {
+ notifyItemMoved(from, to);
+ }
}
public class Holder extends ViewHolder {
@@ -491,6 +501,8 @@
mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0);
mTileView.setBackground(null);
mTileView.getIcon().disableAnimation();
+ mTileView.setTag(this);
+ ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate);
}
}
@@ -527,6 +539,46 @@
.setDuration(DRAG_LENGTH)
.alpha(.6f);
}
+
+ boolean canRemove() {
+ return canRemoveFromPosition(getLayoutPosition());
+ }
+
+ boolean canAdd() {
+ return canAddFromPosition(getLayoutPosition());
+ }
+
+ void toggleState() {
+ if (canAdd()) {
+ add();
+ } else {
+ remove();
+ }
+ }
+
+ private void add() {
+ addFromPosition(getLayoutPosition());
+ }
+
+ private void remove() {
+ removeFromPosition(getLayoutPosition());
+ }
+
+ boolean isCurrentTile() {
+ return TileAdapter.this.isCurrentTile(getLayoutPosition());
+ }
+
+ void startAccessibleAdd() {
+ TileAdapter.this.startAccessibleAdd(getLayoutPosition());
+ }
+
+ void startAccessibleMove() {
+ TileAdapter.this.startAccessibleMove(getLayoutPosition());
+ }
+
+ boolean canTakeAccessibleAction() {
+ return mAccessibilityAction == ACTION_NONE;
+ }
}
private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
@@ -648,7 +700,7 @@
to == 0 || to == RecyclerView.NO_POSITION) {
return false;
}
- return move(from, to, target.itemView);
+ return move(from, to);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
new file mode 100644
index 0000000..1e426ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.customize;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Accessibility delegate for {@link TileAdapter} views.
+ *
+ * This delegate will populate the accessibility info with the proper actions that can be taken for
+ * the different tiles:
+ * <ul>
+ * <li>Add to end if the tile is not a current tile (by double tap).</li>
+ * <li>Add to a given position (by context menu). This will let the user select a position.</li>
+ * <li>Remove, if the tile is a current tile (by double tap).</li>
+ * <li>Move to a given position (by context menu). This will let the user select a position.</li>
+ * </ul>
+ *
+ * This only handles generating the associated actions. The logic for selecting positions is handled
+ * by {@link TileAdapter}.
+ *
+ * In order for the delegate to work properly, the asociated {@link TileAdapter.Holder} should be
+ * passed along with the view using {@link View#setTag}.
+ */
+class TileAdapterDelegate extends AccessibilityDelegateCompat {
+
+ private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position;
+ private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position;
+
+ private TileAdapter.Holder getHolder(View view) {
+ return (TileAdapter.Holder) view.getTag();
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ TileAdapter.Holder holder = getHolder(host);
+ info.setCollectionItemInfo(null);
+ info.setStateDescription("");
+ if (holder == null || !holder.canTakeAccessibleAction()) {
+ // If there's not a holder (not a regular Tile) or an action cannot be taken
+ // because we are in the middle of an accessibility action, don't create a special node.
+ return;
+ }
+
+ addClickAction(host, info, holder);
+ maybeAddActionAddToPosition(host, info, holder);
+ maybeAddActionMoveToPosition(host, info, holder);
+
+ if (holder.isCurrentTile()) {
+ info.setStateDescription(host.getContext().getString(
+ R.string.accessibility_qs_edit_position, holder.getLayoutPosition()));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ TileAdapter.Holder holder = getHolder(host);
+
+ if (holder == null || !holder.canTakeAccessibleAction()) {
+ // If there's not a holder (not a regular Tile) or an action cannot be taken
+ // because we are in the middle of an accessibility action, perform the default action.
+ return super.performAccessibilityAction(host, action, args);
+ }
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ holder.toggleState();
+ return true;
+ } else if (action == MOVE_TO_POSITION_ID) {
+ holder.startAccessibleMove();
+ return true;
+ } else if (action == ADD_TO_POSITION_ID) {
+ holder.startAccessibleAdd();
+ return true;
+ } else {
+ return super.performAccessibilityAction(host, action, args);
+ }
+ }
+
+ private void addClickAction(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ String clickActionString;
+ if (holder.canAdd()) {
+ clickActionString = host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_add_action);
+ } else if (holder.canRemove()) {
+ clickActionString = host.getContext().getString(
+ R.string.accessibility_qs_edit_remove_tile_action);
+ } else {
+ // Remove the default click action if tile can't either be added or removed (for example
+ // if there's the minimum number of tiles)
+ List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> listOfActions =
+ info.getActionList(); // This is a copy
+ int numActions = listOfActions.size();
+ for (int i = 0; i < numActions; i++) {
+ if (listOfActions.get(i).getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ info.removeAction(listOfActions.get(i));
+ }
+ }
+ return;
+ }
+
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfo.ACTION_CLICK, clickActionString);
+ info.addAction(action);
+ }
+
+ private void maybeAddActionMoveToPosition(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ if (holder.isCurrentTile()) {
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(MOVE_TO_POSITION_ID,
+ host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_start_move));
+ info.addAction(action);
+ }
+ }
+
+ private void maybeAddActionAddToPosition(
+ View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) {
+ if (holder.canAdd()) {
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(ADD_TO_POSITION_ID,
+ host.getContext().getString(
+ R.string.accessibility_qs_edit_tile_start_add));
+ info.addAction(action);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index ff7793d..4699ace 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -167,7 +167,7 @@
peopleHubSubscription = null
peopleHeaderView = reinflateView(peopleHeaderView, layoutInflater, R.layout.people_strip)
.apply {
- setOnHeaderClickListener(View.OnClickListener { onGentleHeaderClick() })
+ setOnHeaderClickListener(View.OnClickListener { onPeopleHeaderClick() })
}
if (ENABLE_SNOOZED_CONVERSATION_HUB) {
peopleHubSubscription = peopleHubViewAdapter.bindView(peopleHubViewBoundary)
@@ -522,6 +522,15 @@
Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
+ private fun onPeopleHeaderClick() {
+ val intent = Intent(Settings.ACTION_CONVERSATION_SETTINGS)
+ activityStarter.startActivity(
+ intent,
+ true,
+ true,
+ Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ }
+
private fun onClearGentleNotifsClick(v: View) {
onClearSilentNotifsClickListener?.onClick(v)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 541c784..7447335 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -29,6 +29,7 @@
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.FooterView;
@@ -687,15 +688,27 @@
AmbientState ambientState) {
int childCount = algorithmState.visibleChildren.size();
float childrenOnTop = 0.0f;
+
+ int topHunIndex = -1;
+ for (int i = 0; i < childCount; i++) {
+ ExpandableView child = algorithmState.visibleChildren.get(i);
+ if (child instanceof ActivatableNotificationView
+ && (child.isAboveShelf() || child.showingPulsing())) {
+ topHunIndex = i;
+ break;
+ }
+ }
+
for (int i = childCount - 1; i >= 0; i--) {
childrenOnTop = updateChildZValue(i, childrenOnTop,
- algorithmState, ambientState);
+ algorithmState, ambientState, i == topHunIndex);
}
}
protected float updateChildZValue(int i, float childrenOnTop,
StackScrollAlgorithmState algorithmState,
- AmbientState ambientState) {
+ AmbientState ambientState,
+ boolean shouldElevateHun) {
ExpandableView child = algorithmState.visibleChildren.get(i);
ExpandableViewState childViewState = child.getViewState();
int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
@@ -713,8 +726,7 @@
}
childViewState.zTranslation = baseZ
+ childrenOnTop * zDistanceBetweenElements;
- } else if (child == ambientState.getTrackedHeadsUpRow()
- || (i == 0 && (child.isAboveShelf() || child.showingPulsing()))) {
+ } else if (shouldElevateHun) {
// In case this is a new view that has never been measured before, we don't want to
// elevate if we are currently expanded more then the notification
int shelfHeight = ambientState.getShelf() == null ? 0 :
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index c43ad36..82ad00a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -359,6 +360,11 @@
return false;
}
+ private static boolean isOngoingCallNotif(NotificationEntry entry) {
+ return entry.getSbn().isOngoing() && Notification.CATEGORY_CALL.equals(
+ entry.getSbn().getNotification().category);
+ }
+
/**
* This represents a notification and how long it is in a heads up mode. It also manages its
* lifecycle automatically when created.
@@ -391,6 +397,15 @@
return 1;
}
+ boolean selfCall = isOngoingCallNotif(mEntry);
+ boolean otherCall = isOngoingCallNotif(headsUpEntry.mEntry);
+
+ if (selfCall && !otherCall) {
+ return -1;
+ } else if (!selfCall && otherCall) {
+ return 1;
+ }
+
if (remoteInputActive && !headsUpEntry.remoteInputActive) {
return -1;
} else if (!remoteInputActive && headsUpEntry.remoteInputActive) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 15828b4..1ad8856 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -999,6 +999,29 @@
verify(mNotificationGroupManager, times(1)).onEntryRemoved(groupSummary.getEntry());
}
+
+ /**
+ * Verifies that when a non visually interruptive update occurs for a bubble in the overflow,
+ * the that bubble does not get promoted from the overflow.
+ */
+ @Test
+ public void test_notVisuallyInterruptive_updateOverflowBubble_notAdded() {
+ // Setup
+ mBubbleController.updateBubble(mRow.getEntry());
+ mBubbleController.updateBubble(mRow2.getEntry());
+ assertTrue(mBubbleController.hasBubbles());
+
+ // Overflow it
+ mBubbleData.dismissBubbleWithKey(mRow.getEntry().getKey(),
+ BubbleController.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getEntry().getKey())).isFalse();
+ assertThat(mBubbleData.hasOverflowBubbleWithKey(mRow.getEntry().getKey())).isTrue();
+
+ // Test
+ mBubbleController.updateBubble(mRow.getEntry());
+ assertThat(mBubbleData.hasBubbleInStackWithKey(mRow.getEntry().getKey())).isFalse();
+ }
+
/**
* Sets the bubble metadata flags for this entry. These ]flags are normally set by
* NotificationManagerService when the notification is sent, however, these tests do not
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index 315caee..4bbc41e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -513,6 +513,26 @@
}
/**
+ * Verifies that when a non visually interruptive update occurs, that the selection does not
+ * change.
+ */
+ @Test
+ public void test_notVisuallyInterruptive_updateBubble_selectionDoesntChange() {
+ // Setup
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryB1, 2000);
+ sendUpdatedEntryAtTime(mEntryB2, 3000);
+ sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
+ mBubbleData.setListener(mListener);
+
+ assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2);
+
+ // Test
+ sendUpdatedEntryAtTime(mEntryB1, 5000, false /* isVisuallyInterruptive */);
+ assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2);
+ }
+
+ /**
* Verifies that a request to expand the stack has no effect if there are no bubbles.
*/
@Test
@@ -883,9 +903,15 @@
}
private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) {
+ sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */);
+ }
+
+ private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime,
+ boolean visuallyInterruptive) {
setPostTime(entry, postTime);
// BubbleController calls this:
Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */);
+ b.setVisuallyInterruptiveForTest(visuallyInterruptive);
// And then this
mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/,
true /* showInShade */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 8a30b00..81139f19 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -43,7 +43,6 @@
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import dagger.Lazy
@@ -53,7 +52,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
import org.mockito.Mockito.anyBoolean
@@ -203,7 +201,7 @@
fun bindWhenUnattached() {
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, null, null, device, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
assertThat(player.isPlaying()).isFalse()
}
@@ -212,7 +210,7 @@
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
assertThat(appName.getText()).isEqualTo(APP)
assertThat(titleText.getText()).isEqualTo(TITLE)
assertThat(artistText.getText()).isEqualTo(ARTIST)
@@ -223,7 +221,7 @@
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
verify(view).setBackgroundTintList(list.capture())
assertThat(list.value).isEqualTo(ColorStateList.valueOf(BG_COLOR))
@@ -234,7 +232,7 @@
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isTrue()
}
@@ -246,7 +244,7 @@
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
verify(expandedSet).setVisibility(seamless.id, View.GONE)
verify(expandedSet).setVisibility(seamlessFallback.id, View.VISIBLE)
verify(collapsedSet).setVisibility(seamless.id, View.GONE)
@@ -258,7 +256,7 @@
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
- player.bind(state)
+ player.bind(state, PACKAGE)
assertThat(seamless.isEnabled()).isTrue()
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
com.android.internal.R.string.ext_media_seamless_action))
@@ -270,7 +268,7 @@
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null,
resumption = true)
- player.bind(state)
+ player.bind(state, PACKAGE)
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isFalse()
}
@@ -322,31 +320,18 @@
@Test
fun dismissButtonClick() {
+ val mediaKey = "key for dismissal"
player.attach(holder)
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
notificationKey = KEY)
- player.bind(state)
+ player.bind(state, mediaKey)
dismiss.callOnClick()
val captor = ArgumentCaptor.forClass(ActivityStarter.OnDismissAction::class.java)
verify(keyguardDismissUtil).executeWhenUnlocked(captor.capture(), anyBoolean())
captor.value.onDismiss()
- verify(mediaDataManager).dismissMediaData(eq(KEY), anyLong())
- }
-
- @Test
- fun dismissButtonClick_nullNotificationKey() {
- player.attach(holder)
- val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
- emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
- player.bind(state)
-
- verify(keyguardDismissUtil, never())
- .executeWhenUnlocked(
- any(ActivityStarter.OnDismissAction::class.java),
- ArgumentMatchers.anyBoolean()
- )
+ verify(mediaDataManager).dismissMediaData(eq(mediaKey), anyLong())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
new file mode 100644
index 0000000..5d81de6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.graphics.Color
+import android.media.MediaDescription
+import android.media.session.MediaSession
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.tuner.TunerService
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.After
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+private const val KEY = "TEST_KEY"
+private const val OLD_KEY = "RESUME_KEY"
+private const val APP = "APP"
+private const val BG_COLOR = Color.RED
+private const val PACKAGE_NAME = "PKG"
+private const val CLASS_NAME = "CLASS"
+private const val ARTIST = "ARTIST"
+private const val TITLE = "TITLE"
+private const val USER_ID = 0
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val RESUME_COMPONENTS = "package1/class1:package2/class2:package3/class3"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaResumeListenerTest : SysuiTestCase() {
+
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var mediaDataManager: MediaDataManager
+ @Mock private lateinit var device: MediaDeviceData
+ @Mock private lateinit var token: MediaSession.Token
+ @Mock private lateinit var tunerService: TunerService
+ @Mock private lateinit var resumeBrowserFactory: ResumeMediaBrowserFactory
+ @Mock private lateinit var resumeBrowser: ResumeMediaBrowser
+ @Mock private lateinit var sharedPrefs: SharedPreferences
+ @Mock private lateinit var sharedPrefsEditor: SharedPreferences.Editor
+ @Mock private lateinit var mockContext: Context
+ @Mock private lateinit var pendingIntent: PendingIntent
+
+ @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback>
+
+ private lateinit var executor: FakeExecutor
+ private lateinit var data: MediaData
+ private lateinit var resumeListener: MediaResumeListener
+
+ private var originalQsSetting = Settings.Global.getInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
+ private var originalResumeSetting = Settings.Secure.getInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ Settings.Global.putInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1)
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 1)
+
+ whenever(resumeBrowserFactory.create(capture(callbackCaptor), any()))
+ .thenReturn(resumeBrowser)
+
+ // resume components are stored in sharedpreferences
+ whenever(mockContext.getSharedPreferences(eq(MEDIA_PREFERENCES), anyInt()))
+ .thenReturn(sharedPrefs)
+ whenever(sharedPrefs.getString(any(), any())).thenReturn(RESUME_COMPONENTS)
+ whenever(sharedPrefs.edit()).thenReturn(sharedPrefsEditor)
+ whenever(sharedPrefsEditor.putString(any(), any())).thenReturn(sharedPrefsEditor)
+ whenever(mockContext.packageManager).thenReturn(context.packageManager)
+ whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
+
+ executor = FakeExecutor(FakeSystemClock())
+ resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor,
+ tunerService, resumeBrowserFactory)
+ resumeListener.setManager(mediaDataManager)
+ mediaDataManager.addListener(resumeListener)
+
+ data = MediaData(
+ userId = USER_ID,
+ initialized = true,
+ backgroundColor = BG_COLOR,
+ app = APP,
+ appIcon = null,
+ artist = ARTIST,
+ song = TITLE,
+ artwork = null,
+ actions = emptyList(),
+ actionsToShowInCompact = emptyList(),
+ packageName = PACKAGE_NAME,
+ token = token,
+ clickIntent = null,
+ device = device,
+ active = true,
+ notificationKey = KEY,
+ resumeAction = null)
+ }
+
+ @After
+ fun tearDown() {
+ Settings.Global.putInt(context.contentResolver,
+ Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, originalQsSetting)
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, originalResumeSetting)
+ }
+
+ @Test
+ fun testWhenNoResumption_doesNothing() {
+ Settings.Secure.putInt(context.contentResolver,
+ Settings.Secure.MEDIA_CONTROLS_RESUME, 0)
+
+ // When listener is created, we do NOT register a user change listener
+ val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService,
+ resumeBrowserFactory)
+ listener.setManager(mediaDataManager)
+ verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver),
+ any(), any(), any())
+
+ // When data is loaded, we do NOT execute or update anything
+ listener.onMediaDataLoaded(KEY, OLD_KEY, data)
+ assertThat(executor.numPending()).isEqualTo(0)
+ verify(mediaDataManager, never()).setResumeAction(any(), any())
+ }
+
+ @Test
+ fun testOnLoad_checksForResume_noService() {
+ // When media data is loaded that has not been checked yet, and does not have a MBS
+ resumeListener.onMediaDataLoaded(KEY, null, data)
+
+ // Then we report back to the manager
+ verify(mediaDataManager).setResumeAction(KEY, null)
+ }
+
+ @Test
+ fun testOnLoad_checksForResume_hasService() {
+ // Set up mocks to successfully find a MBS that returns valid media
+ val pm = mock(PackageManager::class.java)
+ whenever(mockContext.packageManager).thenReturn(pm)
+ val resolveInfo = ResolveInfo()
+ val serviceInfo = ServiceInfo()
+ serviceInfo.packageName = PACKAGE_NAME
+ resolveInfo.serviceInfo = serviceInfo
+ resolveInfo.serviceInfo.name = CLASS_NAME
+ val resumeInfo = listOf(resolveInfo)
+ whenever(pm.queryIntentServices(any(), anyInt())).thenReturn(resumeInfo)
+
+ val description = MediaDescription.Builder().setTitle(TITLE).build()
+ val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ whenever(resumeBrowser.testConnection()).thenAnswer {
+ callbackCaptor.value.addTrack(description, component, resumeBrowser)
+ }
+
+ // When media data is loaded that has not been checked yet, and does have a MBS
+ val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
+ resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+ // Then we test whether the service is valid
+ executor.runAllReady()
+ verify(resumeBrowser).testConnection()
+
+ // And since it is, we report back to the manager
+ verify(mediaDataManager).setResumeAction(eq(KEY), any())
+
+ // But we do not tell it to add new controls
+ verify(mediaDataManager, never())
+ .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), any())
+
+ // Finally, make sure the resume browser disconnected
+ verify(resumeBrowser).disconnect()
+ }
+
+ @Test
+ fun testOnLoad_doesNotCheckAgain() {
+ // When a media data is loaded that has been checked already
+ var dataCopy = data.copy(hasCheckedForResume = true)
+ resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+
+ // Then we should not check it again
+ verify(resumeBrowser, never()).testConnection()
+ verify(mediaDataManager, never()).setResumeAction(KEY, null)
+ }
+
+ @Test
+ fun testOnUserUnlock_loadsTracks() {
+ // Set up mock service to successfully find valid media
+ val description = MediaDescription.Builder().setTitle(TITLE).build()
+ val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ whenever(resumeBrowser.token).thenReturn(token)
+ whenever(resumeBrowser.appIntent).thenReturn(pendingIntent)
+ whenever(resumeBrowser.findRecentMedia()).thenAnswer {
+ callbackCaptor.value.addTrack(description, component, resumeBrowser)
+ }
+
+ // Make sure broadcast receiver is registered
+ resumeListener.setManager(mediaDataManager)
+ verify(broadcastDispatcher).registerReceiver(eq(resumeListener.userChangeReceiver),
+ any(), any(), any())
+
+ // When we get an unlock event
+ val intent = Intent(Intent.ACTION_USER_UNLOCKED)
+ resumeListener.userChangeReceiver.onReceive(context, intent)
+
+ // Then we should attempt to find recent media for each saved component
+ verify(resumeBrowser, times(3)).findRecentMedia()
+
+ // Then since the mock service found media, the manager should be informed
+ verify(mediaDataManager, times(3)).addResumptionControls(anyInt(),
+ any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
index 7a8e4f7..f397959 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -23,8 +23,9 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
@@ -33,7 +34,6 @@
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Captor
import org.mockito.Mock
@@ -63,10 +63,8 @@
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
@Mock private lateinit var mediaController: MediaController
- @Mock private lateinit var executor: DelayableExecutor
+ private lateinit var executor: FakeExecutor
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
- @Mock private lateinit var cancellationRunnable: Runnable
- @Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable>
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
@JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var metadataBuilder: MediaMetadata.Builder
@@ -78,7 +76,7 @@
@Before
fun setup() {
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
- `when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
+ executor = FakeExecutor(FakeSystemClock())
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
mediaTimeoutListener.timeoutCallback = timeoutCallback
@@ -120,7 +118,7 @@
fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
- verify(executor).executeDelayed(capture(timeoutCaptor), anyLong())
+ assertThat(executor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
}
@@ -137,6 +135,17 @@
}
@Test
+ fun testOnMediaDataRemoved_clearsTimeout() {
+ // GIVEN media that is paused
+ mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ assertThat(executor.numPending()).isEqualTo(1)
+ // WHEN the media is removed
+ mediaTimeoutListener.onMediaDataRemoved(KEY)
+ // THEN the timeout runnable is cancelled
+ assertThat(executor.numPending()).isEqualTo(0)
+ }
+
+ @Test
fun testOnMediaDataLoaded_migratesKeys() {
// From not playing
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
@@ -151,7 +160,24 @@
verify(mediaController).registerCallback(anyObject())
// Enqueues callback
- verify(executor).execute(anyObject())
+ assertThat(executor.numPending()).isEqualTo(1)
+ }
+
+ @Test
+ fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
+ // From not playing
+ mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ clearInvocations(mediaController)
+
+ // Migrate, still not playing
+ val playingState = mock(android.media.session.PlaybackState::class.java)
+ `when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
+ `when`(mediaController.playbackState).thenReturn(playingState)
+ mediaTimeoutListener.onMediaDataLoaded("NEWKEY", KEY, mediaData)
+
+ // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
+ // is another scheduled
+ assertThat(executor.numPending()).isEqualTo(1)
}
@Test
@@ -161,7 +187,7 @@
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
- verify(executor).executeDelayed(capture(timeoutCaptor), anyLong())
+ assertThat(executor.numPending()).isEqualTo(1)
}
@Test
@@ -171,7 +197,7 @@
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0L, 0f).build())
- verify(cancellationRunnable).run()
+ assertThat(executor.numPending()).isEqualTo(0)
}
@Test
@@ -179,10 +205,9 @@
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- clearInvocations(cancellationRunnable)
mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
.setState(PlaybackState.STATE_STOPPED, 0L, 0f).build())
- verify(cancellationRunnable, never()).run()
+ assertThat(executor.numPending()).isEqualTo(1)
}
@Test
@@ -190,7 +215,10 @@
// Assuming we're have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- timeoutCaptor.value.run()
+ with(executor) {
+ advanceClockToNext()
+ runAllReady()
+ }
verify(timeoutCallback).invoke(eq(KEY), eq(true))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
new file mode 100644
index 0000000..d26229e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/ResumeMediaBrowserTest.kt
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.content.ComponentName
+import android.content.Context
+import android.media.MediaDescription
+import android.media.browse.MediaBrowser
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.service.media.MediaBrowserService
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
+
+private const val PACKAGE_NAME = "package"
+private const val CLASS_NAME = "class"
+private const val TITLE = "song title"
+private const val MEDIA_ID = "media ID"
+private const val ROOT = "media browser root"
+
+private fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+private fun <T> any(): T = Mockito.any<T>()
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class ResumeMediaBrowserTest : SysuiTestCase() {
+
+ private lateinit var resumeBrowser: TestableResumeMediaBrowser
+ private val component = ComponentName(PACKAGE_NAME, CLASS_NAME)
+ private val description = MediaDescription.Builder()
+ .setTitle(TITLE)
+ .setMediaId(MEDIA_ID)
+ .build()
+
+ @Mock lateinit var callback: ResumeMediaBrowser.Callback
+ @Mock lateinit var listener: MediaResumeListener
+ @Mock lateinit var service: MediaBrowserService
+ @Mock lateinit var browserFactory: MediaBrowserFactory
+ @Mock lateinit var browser: MediaBrowser
+ @Mock lateinit var token: MediaSession.Token
+ @Mock lateinit var mediaController: MediaController
+ @Mock lateinit var transportControls: MediaController.TransportControls
+
+ @Captor lateinit var connectionCallback: ArgumentCaptor<MediaBrowser.ConnectionCallback>
+ @Captor lateinit var subscriptionCallback: ArgumentCaptor<MediaBrowser.SubscriptionCallback>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ whenever(browserFactory.create(any(), capture(connectionCallback), any()))
+ .thenReturn(browser)
+
+ whenever(mediaController.transportControls).thenReturn(transportControls)
+
+ resumeBrowser = TestableResumeMediaBrowser(context, callback, component, browserFactory,
+ mediaController)
+ }
+
+ @Test
+ fun testConnection_connectionFails_callsOnError() {
+ // When testConnection cannot connect to the service
+ setupBrowserFailed()
+ resumeBrowser.testConnection()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testConnection_connects_onConnected() {
+ // When testConnection can connect to the service
+ setupBrowserConnection()
+ resumeBrowser.testConnection()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ @Test
+ fun testConnection_noValidMedia_error() {
+ // When testConnection can connect to the service, and does not find valid media
+ setupBrowserConnectionNoResults()
+ resumeBrowser.testConnection()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testConnection_hasValidMedia_addTrack() {
+ // When testConnection can connect to the service, and finds valid media
+ setupBrowserConnectionValidMedia()
+ resumeBrowser.testConnection()
+
+ // Then it calls addTrack
+ verify(callback).onConnected()
+ verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+ }
+
+ @Test
+ fun testFindRecentMedia_connectionFails_error() {
+ // When findRecentMedia is called and we cannot connect
+ setupBrowserFailed()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_noRoot_error() {
+ // When findRecentMedia is called and does not get a valid root
+ setupBrowserConnection()
+ whenever(browser.getRoot()).thenReturn(null)
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_connects_onConnected() {
+ // When findRecentMedia is called and we connect
+ setupBrowserConnection()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ @Test
+ fun testFindRecentMedia_noChildren_error() {
+ // When findRecentMedia is called and we connect, but do not get any results
+ setupBrowserConnectionNoResults()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_notPlayable_error() {
+ // When findRecentMedia is called and we connect, but do not get a playable child
+ setupBrowserConnectionNotPlayable()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testFindRecentMedia_hasValidMedia_addTrack() {
+ // When findRecentMedia is called and we can connect and get playable media
+ setupBrowserConnectionValidMedia()
+ resumeBrowser.findRecentMedia()
+
+ // Then it calls addTrack
+ verify(callback).addTrack(eq(description), eq(component), eq(resumeBrowser))
+ }
+
+ @Test
+ fun testRestart_connectionFails_error() {
+ // When restart is called and we cannot connect
+ setupBrowserFailed()
+ resumeBrowser.restart()
+
+ // Then it calls onError
+ verify(callback).onError()
+ }
+
+ @Test
+ fun testRestart_connects() {
+ // When restart is called and we connect successfully
+ setupBrowserConnection()
+ resumeBrowser.restart()
+
+ // Then it creates a new controller and sends play command
+ verify(transportControls).prepare()
+ verify(transportControls).play()
+
+ // Then it calls onConnected
+ verify(callback).onConnected()
+ }
+
+ /**
+ * Helper function to mock a failed connection
+ */
+ private fun setupBrowserFailed() {
+ whenever(browser.connect()).thenAnswer {
+ connectionCallback.value.onConnectionFailed()
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection only
+ */
+ private fun setupBrowserConnection() {
+ whenever(browser.connect()).thenAnswer {
+ connectionCallback.value.onConnected()
+ }
+ whenever(browser.isConnected()).thenReturn(true)
+ whenever(browser.getRoot()).thenReturn(ROOT)
+ whenever(browser.sessionToken).thenReturn(token)
+ }
+
+ /**
+ * Helper function to mock a successful connection, but no media results
+ */
+ private fun setupBrowserConnectionNoResults() {
+ setupBrowserConnection()
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, emptyList())
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection, but no playable results
+ */
+ private fun setupBrowserConnectionNotPlayable() {
+ setupBrowserConnection()
+
+ val child = MediaBrowser.MediaItem(description, 0)
+
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+ }
+ }
+
+ /**
+ * Helper function to mock a successful connection with playable media
+ */
+ private fun setupBrowserConnectionValidMedia() {
+ setupBrowserConnection()
+
+ val child = MediaBrowser.MediaItem(description, MediaBrowser.MediaItem.FLAG_PLAYABLE)
+
+ whenever(browser.serviceComponent).thenReturn(component)
+ whenever(browser.subscribe(any(), capture(subscriptionCallback))).thenAnswer {
+ subscriptionCallback.value.onChildrenLoaded(ROOT, listOf(child))
+ }
+ }
+
+ /**
+ * Override so media controller use is testable
+ */
+ private class TestableResumeMediaBrowser(
+ context: Context,
+ callback: Callback,
+ componentName: ComponentName,
+ browserFactory: MediaBrowserFactory,
+ private val fakeController: MediaController
+ ) : ResumeMediaBrowser(context, callback, componentName, browserFactory) {
+
+ override fun createMediaController(token: MediaSession.Token): MediaController {
+ return fakeController
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
new file mode 100644
index 0000000..a5dead0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileAdapterDelegateTest.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.customize;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Bundle;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class TileAdapterDelegateTest extends SysuiTestCase {
+
+ private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position;
+ private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position;
+ private static final int POSITION_STRING_ID = R.string.accessibility_qs_edit_position;
+
+ @Mock
+ private TileAdapter.Holder mHolder;
+
+ private AccessibilityNodeInfoCompat mInfo;
+ private TileAdapterDelegate mDelegate;
+ private View mView;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mView = new View(mContext);
+ mDelegate = new TileAdapterDelegate();
+ mInfo = AccessibilityNodeInfoCompat.obtain();
+ }
+
+ @Test
+ public void testInfoNoSpecialActionsWhenNoHolder() {
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : mInfo.getActionList()) {
+ if (action.getId() == MOVE_TO_POSITION_ID || action.getId() == ADD_TO_POSITION_ID
+ || action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ fail("It should not have special action " + action.getId());
+ }
+ }
+ }
+
+ @Test
+ public void testInfoNoSpecialActionsWhenCannotStartAccessibleAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(false);
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action : mInfo.getActionList()) {
+ if (action.getId() == MOVE_TO_POSITION_ID || action.getId() == ADD_TO_POSITION_ID
+ || action.getId() == AccessibilityNodeInfo.ACTION_CLICK) {
+ fail("It should not have special action " + action.getId());
+ }
+ }
+ }
+
+ @Test
+ public void testNoCollectionItemInfo() {
+ mInfo.setCollectionItemInfo(
+ AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(0, 1, 0, 1, false));
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getCollectionItemInfo()).isNull();
+ }
+
+ @Test
+ public void testStateDescriptionHasPositionForCurrentTile() {
+ mView.setTag(mHolder);
+ int position = 3;
+ when(mHolder.getLayoutPosition()).thenReturn(position);
+ when(mHolder.isCurrentTile()).thenReturn(true);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ String expectedString = mContext.getString(POSITION_STRING_ID, position);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getStateDescription()).isEqualTo(expectedString);
+ }
+
+ @Test
+ public void testStateDescriptionEmptyForNotCurrentTile() {
+ mView.setTag(mHolder);
+ int position = 3;
+ when(mHolder.getLayoutPosition()).thenReturn(position);
+ when(mHolder.isCurrentTile()).thenReturn(false);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(mInfo.getStateDescription()).isEqualTo("");
+ }
+
+ @Test
+ public void testClickAddAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(true);
+ when(mHolder.canRemove()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ String expectedString = mContext.getString(R.string.accessibility_qs_edit_tile_add_action);
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action.getLabel().toString()).contains(expectedString);
+ }
+
+ @Test
+ public void testClickRemoveAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+ when(mHolder.canRemove()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ String expectedString = mContext.getString(
+ R.string.accessibility_qs_edit_remove_tile_action);
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action.getLabel().toString()).contains(expectedString);
+ }
+
+ @Test
+ public void testNoClickAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+ when(mHolder.canRemove()).thenReturn(false);
+ mInfo.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat action =
+ getActionForId(mInfo, AccessibilityNodeInfo.ACTION_CLICK);
+ assertThat(action).isNull();
+ }
+
+ @Test
+ public void testAddToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, ADD_TO_POSITION_ID)).isNotNull();
+ }
+
+ @Test
+ public void testNoAddToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.canAdd()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, ADD_TO_POSITION_ID)).isNull();
+ }
+
+ @Test
+ public void testMoveToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.isCurrentTile()).thenReturn(true);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, MOVE_TO_POSITION_ID)).isNotNull();
+ }
+
+ @Test
+ public void testNoMoveToPositionAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+ when(mHolder.isCurrentTile()).thenReturn(false);
+
+ mDelegate.onInitializeAccessibilityNodeInfo(mView, mInfo);
+ assertThat(getActionForId(mInfo, MOVE_TO_POSITION_ID)).isNull();
+ }
+
+ @Test
+ public void testNoInteractionsWhenCannotTakeAccessibleAction() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(false);
+
+ mDelegate.performAccessibilityAction(mView, AccessibilityNodeInfo.ACTION_CLICK, null);
+ mDelegate.performAccessibilityAction(mView, MOVE_TO_POSITION_ID, new Bundle());
+ mDelegate.performAccessibilityAction(mView, ADD_TO_POSITION_ID, new Bundle());
+
+ verify(mHolder, never()).toggleState();
+ verify(mHolder, never()).startAccessibleAdd();
+ verify(mHolder, never()).startAccessibleMove();
+ }
+
+ @Test
+ public void testClickActionTogglesState() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, AccessibilityNodeInfo.ACTION_CLICK, null);
+
+ verify(mHolder).toggleState();
+ }
+
+ @Test
+ public void testAddToPositionActionStartsAccessibleAdd() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, ADD_TO_POSITION_ID, null);
+
+ verify(mHolder).startAccessibleAdd();
+ }
+
+ @Test
+ public void testMoveToPositionActionStartsAccessibleMove() {
+ mView.setTag(mHolder);
+ when(mHolder.canTakeAccessibleAction()).thenReturn(true);
+
+ mDelegate.performAccessibilityAction(mView, MOVE_TO_POSITION_ID, null);
+
+ verify(mHolder).startAccessibleMove();
+ }
+
+ private AccessibilityNodeInfoCompat.AccessibilityActionCompat getActionForId(
+ AccessibilityNodeInfoCompat info, int action) {
+ for (AccessibilityNodeInfoCompat.AccessibilityActionCompat a : info.getActionList()) {
+ if (a.getId() == action) {
+ return a;
+ }
+ }
+ return null;
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
index 402a99d..dee6020 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
@@ -108,11 +108,7 @@
return new TestableAlertingNotificationManager();
}
- protected StatusBarNotification createNewNotification(int id) {
- Notification.Builder n = new Notification.Builder(mContext, "")
- .setSmallIcon(R.drawable.ic_person)
- .setContentTitle("Title")
- .setContentText("Text");
+ protected StatusBarNotification createNewSbn(int id, Notification.Builder n) {
return new StatusBarNotification(
TEST_PACKAGE_NAME /* pkg */,
TEST_PACKAGE_NAME,
@@ -126,6 +122,14 @@
0 /* postTime */);
}
+ protected StatusBarNotification createNewNotification(int id) {
+ Notification.Builder n = new Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle("Title")
+ .setContentText("Text");
+ return createNewSbn(id, n);
+ }
+
@Before
public void setUp() {
mTestHandler = Handler.createAsync(Looper.myLooper());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
index fc7d0ce..0e4b053 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.policy;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
+import android.app.Notification;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -30,6 +33,7 @@
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.AlertingNotificationManagerTest;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import org.junit.Before;
import org.junit.Test;
@@ -84,5 +88,25 @@
assertTrue("Heads up should live long enough", mLivesPastNormalTime);
assertFalse(mHeadsUpManager.isAlerting(mEntry.getKey()));
}
+
+ @Test
+ public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
+ HeadsUpManager.HeadsUpEntry ongoingCall = mHeadsUpManager.new HeadsUpEntry();
+ ongoingCall.setEntry(new NotificationEntryBuilder()
+ .setSbn(createNewSbn(0,
+ new Notification.Builder(mContext, "")
+ .setCategory(Notification.CATEGORY_CALL)
+ .setOngoing(true)))
+ .build());
+
+ HeadsUpManager.HeadsUpEntry activeRemoteInput = mHeadsUpManager.new HeadsUpEntry();
+ activeRemoteInput.setEntry(new NotificationEntryBuilder()
+ .setSbn(createNewNotification(1))
+ .build());
+ activeRemoteInput.remoteInputActive = true;
+
+ assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0);
+ assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0);
+ }
}
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index 829fca6..9fc8f0b 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -454,10 +454,14 @@
public boolean mayObservePackage(String packageName) {
PackageManager pm = mContext.getPackageManager();
try {
- // A package is a Mainline module if this is non-null
+ // A package is a module if this is non-null
if (pm.getModuleInfo(packageName, 0) != null) {
return true;
}
+ } catch (PackageManager.NameNotFoundException ignore) {
+ }
+
+ try {
ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
} catch (PackageManager.NameNotFoundException e) {
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index e1bb4cd..dd0e1f6 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -453,16 +453,16 @@
}
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
- int callingPid, int callingUid, boolean fgRequired, boolean hideFgNotification,
- String callingPackage, @Nullable String callingFeatureId, final int userId)
+ int callingPid, int callingUid, boolean fgRequired, String callingPackage,
+ @Nullable String callingFeatureId, final int userId)
throws TransactionTooLargeException {
return startServiceLocked(caller, service, resolvedType, callingPid, callingUid, fgRequired,
- hideFgNotification, callingPackage, callingFeatureId, userId, false);
+ callingPackage, callingFeatureId, userId, false);
}
ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
- int callingPid, int callingUid, boolean fgRequired, boolean hideFgNotification,
- String callingPackage, @Nullable String callingFeatureId, final int userId,
+ int callingPid, int callingUid, boolean fgRequired, String callingPackage,
+ @Nullable String callingFeatureId, final int userId,
boolean allowBackgroundActivityStarts) throws TransactionTooLargeException {
if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "startService: " + service
+ " type=" + resolvedType + " args=" + service.getExtras());
@@ -609,7 +609,6 @@
r.startRequested = true;
r.delayedStop = false;
r.fgRequired = fgRequired;
- r.hideFgNotification = hideFgNotification;
r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
service, neededGrants, callingUid));
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 491579a..3b85180 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1438,10 +1438,6 @@
final Injector mInjector;
- /** The package verifier app. */
- private String mPackageVerifier;
- private int mPackageVerifierUid = UserHandle.USER_NULL;
-
static final class ProcessChangeItem {
static final int CHANGE_ACTIVITIES = 1<<0;
static final int CHANGE_FOREGROUND_SERVICES = 1<<1;
@@ -2350,18 +2346,6 @@
if (phase == PHASE_SYSTEM_SERVICES_READY) {
mService.mBatteryStatsService.systemServicesReady();
mService.mServices.systemServicesReady();
- mService.mPackageVerifier = ArrayUtils.firstOrNull(
- LocalServices.getService(PackageManagerInternal.class).getKnownPackageNames(
- PackageManagerInternal.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM));
- if (mService.mPackageVerifier != null) {
- try {
- mService.mPackageVerifierUid =
- getContext().getPackageManager().getPackageUid(
- mService.mPackageVerifier, UserHandle.USER_SYSTEM);
- } catch (NameNotFoundException e) {
- Slog.wtf(TAG, "Package manager couldn't get package verifier uid", e);
- }
- }
} else if (phase == PHASE_ACTIVITY_MANAGER_READY) {
mService.startBroadcastObservers();
} else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
@@ -10530,7 +10514,7 @@
private void dumpEverything(FileDescriptor fd, PrintWriter pw, String[] args, int opti,
boolean dumpAll, String dumpPackage, boolean dumpClient, boolean dumpNormalPriority,
- int dumpAppId) {
+ int dumpAppId, boolean dumpProxies) {
ActiveServices.ServiceDumper sdumper;
@@ -10589,7 +10573,7 @@
}
sdumper.dumpWithClient();
}
- if (dumpPackage == null) {
+ if (dumpPackage == null && dumpProxies) {
// Intentionally dropping the lock for this, because dumpBinderProxies() will make many
// outgoing binder calls to retrieve interface descriptors; while that is system code,
// there is nothing preventing an app from overriding this implementation by talking to
@@ -10998,13 +10982,14 @@
// dumpEverything() will take the lock when needed, and momentarily drop
// it for dumping client state.
dumpEverything(fd, pw, args, opti, dumpAll, dumpPackage, dumpClient,
- dumpNormalPriority, dumpAppId);
+ dumpNormalPriority, dumpAppId, true /* dumpProxies */);
} else {
// Take the lock here, so we get a consistent state for the entire dump;
- // dumpEverything() will take the lock as well, but that is fine.
+ // dumpEverything() will take the lock as well, which is fine for everything
+ // except dumping proxies, which can take a long time; exclude them.
synchronized(this) {
dumpEverything(fd, pw, args, opti, dumpAll, dumpPackage, dumpClient,
- dumpNormalPriority, dumpAppId);
+ dumpNormalPriority, dumpAppId, false /* dumpProxies */);
}
}
}
@@ -15007,8 +14992,8 @@
@Override
public ComponentName startService(IApplicationThread caller, Intent service,
- String resolvedType, boolean requireForeground, boolean hideForegroundNotification,
- String callingPackage, String callingFeatureId, int userId)
+ String resolvedType, boolean requireForeground, String callingPackage,
+ String callingFeatureId, int userId)
throws TransactionTooLargeException {
enforceNotIsolatedCaller("startService");
// Refuse possible leaked file descriptors
@@ -15020,27 +15005,17 @@
throw new IllegalArgumentException("callingPackage cannot be null");
}
- final int callingUid = Binder.getCallingUid();
- if (requireForeground && hideForegroundNotification) {
- if (!UserHandle.isSameApp(callingUid, mPackageVerifierUid)
- || !callingPackage.equals(mPackageVerifier)) {
- throw new IllegalArgumentException(
- "Only the package verifier can hide its foreground service notification");
- }
- Slog.i(TAG, "Foreground service notification hiding requested by " + callingPackage);
- }
-
if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,
"*** startService: " + service + " type=" + resolvedType + " fg=" + requireForeground);
synchronized(this) {
final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
ComponentName res;
try {
res = mServices.startServiceLocked(caller, service,
resolvedType, callingPid, callingUid,
- requireForeground, hideForegroundNotification,
- callingPackage, callingFeatureId, userId);
+ requireForeground, callingPackage, callingFeatureId, userId);
} finally {
Binder.restoreCallingIdentity(origId);
}
@@ -19480,7 +19455,7 @@
ComponentName res;
try {
res = mServices.startServiceLocked(null, service,
- resolvedType, -1, uid, fgRequired, false, callingPackage,
+ resolvedType, -1, uid, fgRequired, callingPackage,
callingFeatureId, userId, allowBackgroundActivityStarts);
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index a512cca..149e3ba 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -654,7 +654,7 @@
pw.println("Starting service: " + intent);
pw.flush();
ComponentName cn = mInterface.startService(null, intent, intent.getType(),
- asForeground, false, SHELL_PACKAGE_NAME, null, mUserId);
+ asForeground, SHELL_PACKAGE_NAME, null, mUserId);
if (cn == null) {
err.println("Error: Not found; no service started.");
return -1;
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 19d5a31..1b65dba 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -104,7 +104,6 @@
boolean whitelistManager; // any bindings to this service have BIND_ALLOW_WHITELIST_MANAGEMENT?
boolean delayed; // are we waiting to start this service in the background?
boolean fgRequired; // is the service required to go foreground after starting?
- boolean hideFgNotification; // Hide the fg service notification
boolean fgWaiting; // is a timeout for going foreground already scheduled?
boolean isForeground; // is service currently in foreground mode?
int foregroundId; // Notification ID of last foreground req.
@@ -824,9 +823,6 @@
}
public void postNotification() {
- if (hideFgNotification) {
- return;
- }
final int appUid = appInfo.uid;
final int appPid = app.pid;
if (foregroundId != 0 && foregroundNoti != null) {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 75c2760..7bb3e36 100755
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -6490,14 +6490,17 @@
if (msg.obj == null) {
break;
}
- // If the app corresponding to this mode death handler object is not
- // capturing or playing audio anymore after 3 seconds, remove it
- // from the stack. Otherwise, check again in 3 seconds.
+ // If no other app is currently owning the audio mode and
+ // the app corresponding to this mode death handler object is still in the
+ // mode owner stack but not capturing or playing audio after 3 seconds,
+ // remove it from the stack.
+ // Otherwise, check again in 3 seconds.
SetModeDeathHandler h = (SetModeDeathHandler) msg.obj;
if (mSetModeDeathHandlers.indexOf(h) < 0) {
break;
}
- if (mRecordMonitor.isRecordingActiveForUid(h.getUid())
+ if (getModeOwnerUid() != h.getUid()
+ || mRecordMonitor.isRecordingActiveForUid(h.getUid())
|| mPlaybackMonitor.isPlaybackActiveForUid(h.getUid())) {
sendMsg(mAudioHandler,
MSG_CHECK_MODE_FOR_UID,
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 254285d..7cce78b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1700,7 +1700,8 @@
Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
.setPackage(mContext.getPackageName());
- mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
mShowOngoingImeSwitcherForPhones = false;
@@ -2529,7 +2530,8 @@
mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.input_method_binding_label);
mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
- mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
+ mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
+ PendingIntent.FLAG_IMMUTABLE));
if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
mLastBindTime = SystemClock.uptimeMillis();
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index d6557f6..b3eb531 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -96,7 +96,9 @@
import static com.android.internal.util.XmlUtils.readIntAttribute;
import static com.android.internal.util.XmlUtils.readLongAttribute;
import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.readThisIntArrayXml;
import static com.android.internal.util.XmlUtils.writeBooleanAttribute;
+import static com.android.internal.util.XmlUtils.writeIntArrayXml;
import static com.android.internal.util.XmlUtils.writeIntAttribute;
import static com.android.internal.util.XmlUtils.writeLongAttribute;
import static com.android.internal.util.XmlUtils.writeStringAttribute;
@@ -229,6 +231,7 @@
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.StatLogger;
+import com.android.internal.util.XmlUtils;
import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
@@ -239,6 +242,7 @@
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
@@ -313,7 +317,8 @@
private static final int VERSION_ADDED_NETWORK_ID = 9;
private static final int VERSION_SWITCH_UID = 10;
private static final int VERSION_ADDED_CYCLE = 11;
- private static final int VERSION_LATEST = VERSION_ADDED_CYCLE;
+ private static final int VERSION_ADDED_NETWORK_TYPES = 12;
+ private static final int VERSION_LATEST = VERSION_ADDED_NETWORK_TYPES;
@VisibleForTesting
public static final int TYPE_WARNING = SystemMessage.NOTE_NET_WARNING;
@@ -332,6 +337,7 @@
private static final String TAG_WHITELIST = "whitelist";
private static final String TAG_RESTRICT_BACKGROUND = "restrict-background";
private static final String TAG_REVOKED_RESTRICT_BACKGROUND = "revoked-restrict-background";
+ private static final String TAG_XML_UTILS_INT_ARRAY = "int-array";
private static final String ATTR_VERSION = "version";
private static final String ATTR_RESTRICT_BACKGROUND = "restrictBackground";
@@ -360,6 +366,8 @@
private static final String ATTR_USAGE_BYTES = "usageBytes";
private static final String ATTR_USAGE_TIME = "usageTime";
private static final String ATTR_OWNER_PACKAGE = "ownerPackage";
+ private static final String ATTR_NETWORK_TYPES = "networkTypes";
+ private static final String ATTR_XML_UTILS_NAME = "name";
private static final String ACTION_ALLOW_BACKGROUND =
"com.android.server.net.action.ALLOW_BACKGROUND";
@@ -2311,13 +2319,25 @@
}
final int subId = readIntAttribute(in, ATTR_SUB_ID);
+ final String ownerPackage = readStringAttribute(in, ATTR_OWNER_PACKAGE);
+
+ if (version >= VERSION_ADDED_NETWORK_TYPES) {
+ final int depth = in.getDepth();
+ while (XmlUtils.nextElementWithin(in, depth)) {
+ if (TAG_XML_UTILS_INT_ARRAY.equals(in.getName())
+ && ATTR_NETWORK_TYPES.equals(
+ readStringAttribute(in, ATTR_XML_UTILS_NAME))) {
+ final int[] networkTypes =
+ readThisIntArrayXml(in, TAG_XML_UTILS_INT_ARRAY, null);
+ builder.setNetworkTypes(networkTypes);
+ }
+ }
+ }
+
final SubscriptionPlan plan = builder.build();
mSubscriptionPlans.put(subId, ArrayUtils.appendElement(
SubscriptionPlan.class, mSubscriptionPlans.get(subId), plan));
-
- final String ownerPackage = readStringAttribute(in, ATTR_OWNER_PACKAGE);
mSubscriptionPlansOwner.put(subId, ownerPackage);
-
} else if (TAG_UID_POLICY.equals(tag)) {
final int uid = readIntAttribute(in, ATTR_UID);
final int policy = readIntAttribute(in, ATTR_POLICY);
@@ -2513,6 +2533,9 @@
writeIntAttribute(out, ATTR_LIMIT_BEHAVIOR, plan.getDataLimitBehavior());
writeLongAttribute(out, ATTR_USAGE_BYTES, plan.getDataUsageBytes());
writeLongAttribute(out, ATTR_USAGE_TIME, plan.getDataUsageTime());
+ try {
+ writeIntArrayXml(plan.getNetworkTypes(), ATTR_NETWORK_TYPES, out);
+ } catch (XmlPullParserException ignored) { }
out.endTag(null, TAG_SUBSCRIPTION_PLAN);
}
}
@@ -3310,7 +3333,8 @@
// let in core system components (like the Settings app).
final String ownerPackage = mSubscriptionPlansOwner.get(subId);
if (Objects.equals(ownerPackage, callingPackage)
- || (UserHandle.getCallingAppId() == android.os.Process.SYSTEM_UID)) {
+ || (UserHandle.getCallingAppId() == android.os.Process.SYSTEM_UID)
+ || (UserHandle.getCallingAppId() == android.os.Process.PHONE_UID)) {
return mSubscriptionPlans.get(subId);
} else {
Log.w(TAG, "Not returning plans because caller " + callingPackage
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index 3dd82a6..7e6b7cd 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -732,6 +732,11 @@
final ActivityStack stack = task.getStack();
beginDeferResume();
+ // The LaunchActivityItem also contains process configuration, so the configuration change
+ // from WindowProcessController#setProcess can be deferred. The major reason is that if
+ // the activity has FixedRotationAdjustments, it needs to be applied with configuration.
+ // In general, this reduces a binder transaction if process configuration is changed.
+ proc.pauseConfigurationDispatch();
try {
r.startFreezingScreenLocked(proc, 0);
@@ -826,9 +831,9 @@
// Because we could be starting an Activity in the system process this may not go
// across a Binder interface which would create a new Configuration. Consequently
// we have to always create a new Configuration here.
-
+ final Configuration procConfig = proc.prepareConfigurationForLaunchingActivity();
final MergedConfiguration mergedConfiguration = new MergedConfiguration(
- proc.getConfiguration(), r.getMergedOverrideConfiguration());
+ procConfig, r.getMergedOverrideConfiguration());
r.setLastReportedConfiguration(mergedConfiguration);
logIfTransactionTooLarge(r.intent, r.getSavedState());
@@ -862,6 +867,11 @@
// Schedule transaction.
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
+ if (procConfig.seq > mRootWindowContainer.getConfiguration().seq) {
+ // If the seq is increased, there should be something changed (e.g. registered
+ // activity configuration).
+ proc.setLastReportedConfiguration(procConfig);
+ }
if ((proc.mInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0
&& mService.mHasHeavyWeightFeature) {
// This may be a heavy-weight process! Note that the package manager will ensure
@@ -896,6 +906,7 @@
}
} finally {
endDeferResume();
+ proc.resumeConfigurationDispatch();
}
r.launchFailed = false;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index a65eca8..72cd32f 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -48,6 +48,7 @@
import static com.android.server.wm.ActivityStack.ActivityState.RESUMED;
import static com.android.server.wm.ActivityStack.ActivityState.STOPPED;
import static com.android.server.wm.ActivityStack.ActivityState.STOPPING;
+import static com.android.server.wm.ActivityStack.STACK_VISIBILITY_INVISIBLE;
import static com.android.server.wm.ActivityStackSupervisor.DEFER_RESUME;
import static com.android.server.wm.ActivityStackSupervisor.ON_TOP;
import static com.android.server.wm.ActivityStackSupervisor.dumpHistoryList;
@@ -1931,24 +1932,29 @@
}
boolean attachApplication(WindowProcessController app) throws RemoteException {
- final String processName = app.mName;
boolean didSomething = false;
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
- final DisplayContent display = getChildAt(displayNdx);
- final ActivityStack stack = display.getFocusedStack();
- if (stack == null) {
- continue;
- }
-
mTmpRemoteException = null;
mTmpBoolean = false; // Set to true if an activity was started.
- final PooledFunction c = PooledLambda.obtainFunction(
- RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
- PooledLambda.__(ActivityRecord.class), app, stack.topRunningActivity());
- stack.forAllActivities(c);
- c.recycle();
- if (mTmpRemoteException != null) {
- throw mTmpRemoteException;
+
+ final DisplayContent display = getChildAt(displayNdx);
+ for (int areaNdx = display.getTaskDisplayAreaCount() - 1; areaNdx >= 0; --areaNdx) {
+ final TaskDisplayArea taskDisplayArea = display.getTaskDisplayAreaAt(areaNdx);
+ for (int taskNdx = taskDisplayArea.getStackCount() - 1; taskNdx >= 0; --taskNdx) {
+ final ActivityStack rootTask = taskDisplayArea.getStackAt(taskNdx);
+ if (rootTask.getVisibility(null /*starting*/) == STACK_VISIBILITY_INVISIBLE) {
+ break;
+ }
+ final PooledFunction c = PooledLambda.obtainFunction(
+ RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
+ PooledLambda.__(ActivityRecord.class), app,
+ rootTask.topRunningActivity());
+ rootTask.forAllActivities(c);
+ c.recycle();
+ if (mTmpRemoteException != null) {
+ throw mTmpRemoteException;
+ }
+ }
}
didSomething |= mTmpBoolean;
}
@@ -1966,8 +1972,8 @@
}
try {
- if (mStackSupervisor.realStartActivityLocked(r, app, top == r /*andResume*/,
- true /*checkConfig*/)) {
+ if (mStackSupervisor.realStartActivityLocked(r, app,
+ top == r && r.isFocusable() /*andResume*/, true /*checkConfig*/)) {
mTmpBoolean = true;
}
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3a750f2..db3c74f 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3671,6 +3671,10 @@
return STACK_VISIBILITY_INVISIBLE;
}
+ if (isTopActivityLaunchedBehind()) {
+ return STACK_VISIBILITY_VISIBLE;
+ }
+
boolean gotSplitScreenStack = false;
boolean gotOpaqueSplitScreenPrimary = false;
boolean gotOpaqueSplitScreenSecondary = false;
@@ -3788,6 +3792,14 @@
: STACK_VISIBILITY_VISIBLE;
}
+ private boolean isTopActivityLaunchedBehind() {
+ final ActivityRecord top = topRunningActivity();
+ if (top != null && top.mLaunchTaskBehind) {
+ return true;
+ }
+ return false;
+ }
+
ActivityRecord isInTask(ActivityRecord r) {
if (r == null) {
return null;
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index bd959ab..df49ac7 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -186,13 +186,16 @@
// Last configuration that was reported to the process.
private final Configuration mLastReportedConfiguration = new Configuration();
- // Configuration that is waiting to be dispatched to the process.
- private Configuration mPendingConfiguration;
+ /** Whether the process configuration is waiting to be dispatched to the process. */
+ private boolean mHasPendingConfigurationChange;
// Registered display id as a listener to override config change
private int mDisplayId;
private ActivityRecord mConfigActivityRecord;
// Whether the activity config override is allowed for this process.
private volatile boolean mIsActivityConfigOverrideAllowed = true;
+ /** Non-zero to pause dispatching process configuration change. */
+ private int mPauseConfigurationDispatchCount;
+
/**
* Activities that hosts some UI drawn by the current process. The activities live
* in another process. This is used to check if the process is currently showing anything
@@ -1115,8 +1118,10 @@
onMergedOverrideConfigurationChanged(Configuration.EMPTY);
}
- private void registerActivityConfigurationListener(ActivityRecord activityRecord) {
- if (activityRecord == null || activityRecord.containsListener(this)) {
+ void registerActivityConfigurationListener(ActivityRecord activityRecord) {
+ if (activityRecord == null || activityRecord.containsListener(this)
+ // Check for the caller from outside of this class.
+ || !mIsActivityConfigOverrideAllowed) {
return;
}
// A process can only register to one activityRecord to listen to the override configuration
@@ -1168,25 +1173,25 @@
@Override
public void onRequestedOverrideConfigurationChanged(Configuration overrideConfiguration) {
- super.onRequestedOverrideConfigurationChanged(
- sanitizeProcessConfiguration(overrideConfiguration));
+ super.onRequestedOverrideConfigurationChanged(overrideConfiguration);
}
@Override
public void onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig) {
- super.onRequestedOverrideConfigurationChanged(
- sanitizeProcessConfiguration(mergedOverrideConfig));
+ super.onRequestedOverrideConfigurationChanged(mergedOverrideConfig);
}
- private static Configuration sanitizeProcessConfiguration(Configuration config) {
+ @Override
+ void resolveOverrideConfiguration(Configuration newParentConfig) {
+ super.resolveOverrideConfiguration(newParentConfig);
+ final Configuration resolvedConfig = getResolvedOverrideConfiguration();
// Make sure that we don't accidentally override the activity type.
- if (config.windowConfiguration.getActivityType() != ACTIVITY_TYPE_UNDEFINED) {
- final Configuration sanitizedConfig = new Configuration(config);
- sanitizedConfig.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
- return sanitizedConfig;
- }
-
- return config;
+ resolvedConfig.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ // Activity has an independent ActivityRecord#mConfigurationSeq. If this process registers
+ // activity configuration, its config seq shouldn't go backwards by activity configuration.
+ // Otherwise if other places send wpc.getConfiguration() to client, the configuration may
+ // be ignored due to the seq is older.
+ resolvedConfig.seq = newParentConfig.seq;
}
private void updateConfiguration() {
@@ -1204,11 +1209,7 @@
if (mListener.isCached()) {
// This process is in a cached state. We will delay delivering the config change to the
// process until the process is no longer cached.
- if (mPendingConfiguration == null) {
- mPendingConfiguration = new Configuration(config);
- } else {
- mPendingConfiguration.setTo(config);
- }
+ mHasPendingConfigurationChange = true;
return;
}
@@ -1216,6 +1217,11 @@
}
private void dispatchConfigurationChange(Configuration config) {
+ if (mPauseConfigurationDispatchCount > 0) {
+ mHasPendingConfigurationChange = true;
+ return;
+ }
+ mHasPendingConfigurationChange = false;
if (mThread == null) {
if (Build.IS_DEBUGGABLE && mHasImeService) {
// TODO (b/135719017): Temporary log for debugging IME service.
@@ -1242,7 +1248,7 @@
}
}
- private void setLastReportedConfiguration(Configuration config) {
+ void setLastReportedConfiguration(Configuration config) {
mLastReportedConfiguration.setTo(config);
}
@@ -1250,6 +1256,30 @@
return mLastReportedConfiguration;
}
+ void pauseConfigurationDispatch() {
+ mPauseConfigurationDispatchCount++;
+ }
+
+ void resumeConfigurationDispatch() {
+ mPauseConfigurationDispatchCount--;
+ }
+
+ /**
+ * This is called for sending {@link android.app.servertransaction.LaunchActivityItem}.
+ * The caller must call {@link #setLastReportedConfiguration} if the delivered configuration
+ * is newer.
+ */
+ Configuration prepareConfigurationForLaunchingActivity() {
+ final Configuration config = getConfiguration();
+ if (mHasPendingConfigurationChange) {
+ mHasPendingConfigurationChange = false;
+ // The global configuration may not change, so the client process may have the same
+ // config seq. This increment ensures that the client won't ignore the configuration.
+ config.seq = mAtm.increaseConfigurationSeqLocked();
+ }
+ return config;
+ }
+
/** Returns the total time (in milliseconds) spent executing in both user and system code. */
public long getCpuTime() {
return (mListener != null) ? mListener.getCpuTime() : 0;
@@ -1341,10 +1371,8 @@
public void onProcCachedStateChanged(boolean isCached) {
if (!isCached) {
synchronized (mAtm.mGlobalLockWithoutBoost) {
- if (mPendingConfiguration != null) {
- final Configuration config = mPendingConfiguration;
- mPendingConfiguration = null;
- dispatchConfigurationChange(config);
+ if (mHasPendingConfigurationChange) {
+ dispatchConfigurationChange(getConfiguration());
}
}
}
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 1716dcd..d86f6c9 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -172,6 +172,12 @@
}
}
}
+
+ /** The state may not only be used by self. Make sure to leave the influence by others. */
+ void disassociate(WindowToken token) {
+ mAssociatedTokens.remove(token);
+ mRotatedContainers.remove(token);
+ }
}
private class DeathRecipient implements IBinder.DeathRecipient {
@@ -531,7 +537,7 @@
void applyFixedRotationTransform(DisplayInfo info, DisplayFrames displayFrames,
Configuration config) {
if (mFixedRotationTransformState != null) {
- cleanUpFixedRotationTransformState(true /* replacing */);
+ mFixedRotationTransformState.disassociate(this);
}
mFixedRotationTransformState = new FixedRotationTransformState(info, displayFrames,
new Configuration(config), mDisplayContent.getRotation());
@@ -539,8 +545,7 @@
mDisplayContent.getDisplayPolicy().simulateLayoutDisplay(displayFrames,
mFixedRotationTransformState.mInsetsState,
mFixedRotationTransformState.mBarContentFrames);
- onConfigurationChanged(getParent().getConfiguration());
- notifyFixedRotationTransform(true /* enabled */);
+ onFixedRotationStatePrepared();
}
/**
@@ -553,12 +558,29 @@
return;
}
if (mFixedRotationTransformState != null) {
- cleanUpFixedRotationTransformState(true /* replacing */);
+ mFixedRotationTransformState.disassociate(this);
}
mFixedRotationTransformState = fixedRotationState;
fixedRotationState.mAssociatedTokens.add(this);
- onConfigurationChanged(getParent().getConfiguration());
+ onFixedRotationStatePrepared();
+ }
+
+ /**
+ * Makes the rotated states take effect for this window container and its client process.
+ * This should only be called when {@link #mFixedRotationTransformState} is non-null.
+ */
+ private void onFixedRotationStatePrepared() {
+ // Send the adjustment info first so when the client receives configuration change, it can
+ // get the rotated display metrics.
notifyFixedRotationTransform(true /* enabled */);
+ // Resolve the rotated configuration.
+ onConfigurationChanged(getParent().getConfiguration());
+ final ActivityRecord r = asActivityRecord();
+ if (r != null && r.hasProcess()) {
+ // The application needs to be configured as in a rotated environment for compatibility.
+ // This registration will send the rotated configuration to its process.
+ r.app.registerActivityConfigurationListener(r);
+ }
}
/**
@@ -609,21 +631,12 @@
// The state is cleared at the end, because it is used to indicate that other windows can
// use seamless rotation when applying rotation to display.
for (int i = state.mAssociatedTokens.size() - 1; i >= 0; i--) {
- state.mAssociatedTokens.get(i).cleanUpFixedRotationTransformState(
- false /* replacing */);
+ final WindowToken token = state.mAssociatedTokens.get(i);
+ token.mFixedRotationTransformState = null;
+ token.notifyFixedRotationTransform(false /* enabled */);
}
}
- private void cleanUpFixedRotationTransformState(boolean replacing) {
- if (replacing && mFixedRotationTransformState.mAssociatedTokens.size() > 1) {
- // The state is not only used by self. Make sure to leave the influence by others.
- mFixedRotationTransformState.mAssociatedTokens.remove(this);
- mFixedRotationTransformState.mRotatedContainers.remove(this);
- }
- mFixedRotationTransformState = null;
- notifyFixedRotationTransform(false /* enabled */);
- }
-
/** Notifies application side to enable or disable the rotation adjustment of display info. */
private void notifyFixedRotationTransform(boolean enabled) {
FixedRotationAdjustments adjustments = null;
@@ -687,8 +700,9 @@
if (!isFixedRotationTransforming()) {
return null;
}
- return new FixedRotationAdjustments(mFixedRotationTransformState.mDisplayInfo.rotation,
- mFixedRotationTransformState.mDisplayInfo.displayCutout);
+ final DisplayInfo displayInfo = mFixedRotationTransformState.mDisplayInfo;
+ return new FixedRotationAdjustments(displayInfo.rotation, displayInfo.appWidth,
+ displayInfo.appHeight, displayInfo.displayCutout);
}
@Override
diff --git a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java
index 236ac84..9e6cf84 100644
--- a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java
+++ b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java
@@ -86,8 +86,8 @@
return;
}
List<ShareTarget> shareTargets = getDirectShareTargets();
- SharesheetModelScorer.computeScore(shareTargets, getShareEventType(mIntentFilter),
- System.currentTimeMillis());
+ SharesheetModelScorer.computeScoreForDirectShare(shareTargets,
+ getShareEventType(mIntentFilter), System.currentTimeMillis());
Collections.sort(shareTargets,
Comparator.comparing(ShareTarget::getScore, reverseOrder())
.thenComparing(t -> t.getAppTarget().getRank()));
diff --git a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java
index c77843c..d4a502d 100644
--- a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java
+++ b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java
@@ -20,6 +20,7 @@
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.usage.UsageEvents;
+import android.provider.DeviceConfig;
import android.util.ArrayMap;
import android.util.Pair;
import android.util.Range;
@@ -27,12 +28,14 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ChooserActivity;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.server.people.data.AppUsageStatsData;
import com.android.server.people.data.DataManager;
import com.android.server.people.data.Event;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -46,6 +49,7 @@
private static final String TAG = "SharesheetModelScorer";
private static final boolean DEBUG = false;
private static final Integer RECENCY_SCORE_COUNT = 6;
+ private static final Integer NATIVE_RANK_COUNT = 2;
private static final float RECENCY_INITIAL_BASE_SCORE = 0.4F;
private static final float RECENCY_SCORE_INITIAL_DECAY = 0.05F;
private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F;
@@ -174,6 +178,77 @@
postProcess(shareTargets, targetsLimit, dataManager, callingUserId);
}
+ /**
+ * Computes ranking score for direct sharing. Update
+ * {@link ShareTargetPredictor.ShareTargetScore}.
+ */
+ static void computeScoreForDirectShare(List<ShareTargetPredictor.ShareTarget> shareTargets,
+ int shareEventType, long now) {
+ computeScore(shareTargets, shareEventType, now);
+ promoteTopNativeRankedShortcuts(shareTargets);
+ }
+
+ /**
+ * Promotes top (NATIVE_RANK_COUNT) shortcuts for each package and class, as per shortcut native
+ * ranking provided by apps.
+ */
+ private static void promoteTopNativeRankedShortcuts(
+ List<ShareTargetPredictor.ShareTarget> shareTargets) {
+ float topShortcutBonus = DeviceConfig.getFloat(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ 0f);
+ float secondTopShortcutBonus = DeviceConfig.getFloat(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.NON_TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ 0f);
+ // Populates a map which key is a packageName and className pair, value is a max heap
+ // containing top (NATIVE_RANK_COUNT) shortcuts as per shortcut native ranking provided
+ // by apps.
+ Map<Pair<String, String>, PriorityQueue<ShareTargetPredictor.ShareTarget>>
+ topNativeRankedShareTargetMap = new ArrayMap<>();
+ for (ShareTargetPredictor.ShareTarget shareTarget : shareTargets) {
+ Pair<String, String> key = new Pair<>(shareTarget.getAppTarget().getPackageName(),
+ shareTarget.getAppTarget().getClassName());
+ if (!topNativeRankedShareTargetMap.containsKey(key)) {
+ topNativeRankedShareTargetMap.put(key,
+ new PriorityQueue<>(NATIVE_RANK_COUNT,
+ Collections.reverseOrder(Comparator.comparingInt(
+ p -> p.getAppTarget().getRank()))));
+ }
+ PriorityQueue<ShareTargetPredictor.ShareTarget> rankMaxHeap =
+ topNativeRankedShareTargetMap.get(key);
+ if (rankMaxHeap.isEmpty() || shareTarget.getAppTarget().getRank()
+ < rankMaxHeap.peek().getAppTarget().getRank()) {
+ if (rankMaxHeap.size() == NATIVE_RANK_COUNT) {
+ rankMaxHeap.poll();
+ }
+ rankMaxHeap.offer(shareTarget);
+ }
+ }
+ for (PriorityQueue<ShareTargetPredictor.ShareTarget> maxHeap :
+ topNativeRankedShareTargetMap.values()) {
+ while (!maxHeap.isEmpty()) {
+ ShareTargetPredictor.ShareTarget target = maxHeap.poll();
+ float bonus = maxHeap.isEmpty() ? topShortcutBonus : secondTopShortcutBonus;
+ target.setScore(probOR(target.getScore(), bonus));
+
+ if (DEBUG) {
+ Slog.d(TAG, String.format(
+ "SharesheetModel: promote top shortcut as per native ranking,"
+ + "packageName: %s, className: %s, shortcutId: %s, bonus:%.2f,"
+ + "total:%.2f",
+ target.getAppTarget().getPackageName(),
+ target.getAppTarget().getClassName(),
+ target.getAppTarget().getShortcutInfo() != null
+ ? target.getAppTarget().getShortcutInfo().getId() : null,
+ bonus,
+ target.getScore()));
+ }
+ }
+ }
+ }
+
private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets,
int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
// Populates a map which key is package name and value is list of shareTargets descended
diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java
index 45fff48..605878d 100644
--- a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -28,9 +29,13 @@
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetId;
import android.app.usage.UsageEvents;
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.util.Range;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.server.people.data.AppUsageStatsData;
import com.android.server.people.data.DataManager;
import com.android.server.people.data.Event;
@@ -121,6 +126,13 @@
private ShareTargetPredictor.ShareTarget mShareTarget5;
private ShareTargetPredictor.ShareTarget mShareTarget6;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget1;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget2;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget3;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget4;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget5;
+ private ShareTargetPredictor.ShareTarget mShareShortcutTarget6;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -154,6 +166,46 @@
new AppTargetId("cls2#pkg3"), PACKAGE_3, UserHandle.of(USER_ID))
.setClassName(CLASS_2).build(),
null, null);
+
+ mShareShortcutTarget1 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg1#1"), buildShortcutInfo(PACKAGE_1, 0, "1"))
+ .setClassName(CLASS_1).setRank(2).build(),
+ mEventHistory1, null);
+ mShareShortcutTarget2 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg1#2"), buildShortcutInfo(PACKAGE_1, 0, "2"))
+ .setClassName(CLASS_1).setRank(1).build(),
+ mEventHistory2, null);
+ mShareShortcutTarget3 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg1#3"), buildShortcutInfo(PACKAGE_1, 0, "3"))
+ .setClassName(CLASS_1).setRank(0).build(),
+ mEventHistory3, null);
+ mShareShortcutTarget4 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg2#1"), buildShortcutInfo(PACKAGE_2, 0, "1"))
+ .setClassName(CLASS_1).setRank(2).build(),
+ mEventHistory4, null);
+ mShareShortcutTarget5 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg2#2"), buildShortcutInfo(PACKAGE_2, 0, "2"))
+ .setClassName(CLASS_1).setRank(1).build(),
+ mEventHistory5, null);
+ mShareShortcutTarget6 = new ShareTargetPredictor.ShareTarget(
+ new AppTarget.Builder(
+ new AppTargetId("cls1#pkg2#3"), buildShortcutInfo(PACKAGE_2, 0, "3"))
+ .setClassName(CLASS_1).setRank(3).build(),
+ null, null);
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ Float.toString(0f),
+ true /* makeDefault*/);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.NON_TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ Float.toString(0f),
+ true /* makeDefault*/);
}
@Test
@@ -433,6 +485,101 @@
assertEquals(0f, mShareTarget6.getScore(), DELTA);
}
+ @Test
+ public void testComputeScoreForDirectShare() {
+ // Frequency and recency
+ when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1);
+ when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2);
+ when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3);
+ when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4);
+ when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5);
+
+ when(mEventIndex1.getActiveTimeSlots()).thenReturn(
+ List.of(WITHIN_ONE_DAY, TWO_DAYS_AGO, FIVE_DAYS_AGO));
+ when(mEventIndex2.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO));
+ when(mEventIndex3.getActiveTimeSlots()).thenReturn(List.of(FIVE_DAYS_AGO, TWENTY_DAYS_AGO));
+ when(mEventIndex4.getActiveTimeSlots()).thenReturn(
+ List.of(EIGHT_DAYS_AGO, TWELVE_DAYS_AGO, FOUR_WEEKS_AGO));
+ when(mEventIndex5.getActiveTimeSlots()).thenReturn(List.of());
+
+ when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY);
+ when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO);
+ when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO);
+ when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO);
+ when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null);
+
+ // Frequency of the same mime type
+ when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6);
+ when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7);
+ when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8);
+ when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9);
+ when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10);
+
+ when(mEventIndex6.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO));
+ when(mEventIndex7.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO));
+ when(mEventIndex8.getActiveTimeSlots()).thenReturn(List.of());
+ when(mEventIndex9.getActiveTimeSlots()).thenReturn(List.of(EIGHT_DAYS_AGO));
+ when(mEventIndex10.getActiveTimeSlots()).thenReturn(List.of());
+
+ SharesheetModelScorer.computeScore(
+ List.of(mShareShortcutTarget1, mShareShortcutTarget2, mShareShortcutTarget3,
+ mShareShortcutTarget4, mShareShortcutTarget5, mShareShortcutTarget6),
+ Event.TYPE_SHARE_TEXT,
+ NOW);
+
+ // Verification
+ assertEquals(0.514f, mShareShortcutTarget1.getScore(), DELTA);
+ assertEquals(0.475125f, mShareShortcutTarget2.getScore(), DELTA);
+ assertEquals(0.33f, mShareShortcutTarget3.getScore(), DELTA);
+ assertEquals(0.4411f, mShareShortcutTarget4.getScore(), DELTA);
+ assertEquals(0f, mShareShortcutTarget5.getScore(), DELTA);
+ assertEquals(0f, mShareShortcutTarget6.getScore(), DELTA);
+ }
+
+ @Test
+ public void testComputeScoreForDirectShare_promoteTopNativeRankedShortcuts() {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ Float.toString(0.4f),
+ true /* makeDefault*/);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.NON_TOP_NATIVE_RANKED_SHARING_SHORTCUTS_BOOSTER,
+ Float.toString(0.3f),
+ true /* makeDefault*/);
+
+ when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1);
+ when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2);
+ when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3);
+ when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4);
+ when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5);
+ when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6);
+ when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7);
+ when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8);
+ when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9);
+ when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10);
+
+ SharesheetModelScorer.computeScoreForDirectShare(
+ List.of(mShareShortcutTarget1, mShareShortcutTarget2, mShareShortcutTarget3,
+ mShareShortcutTarget4, mShareShortcutTarget5, mShareShortcutTarget6),
+ Event.TYPE_SHARE_TEXT, 20);
+
+ assertEquals(0f, mShareShortcutTarget1.getScore(), DELTA);
+ assertEquals(0.3f, mShareShortcutTarget2.getScore(), DELTA);
+ assertEquals(0.4f, mShareShortcutTarget3.getScore(), DELTA);
+ assertEquals(0.3f, mShareShortcutTarget4.getScore(), DELTA);
+ assertEquals(0.4f, mShareShortcutTarget5.getScore(), DELTA);
+ assertEquals(0f, mShareShortcutTarget6.getScore(), DELTA);
+ }
+
+ private static ShortcutInfo buildShortcutInfo(String packageName, int userId, String id) {
+ Context mockContext = mock(Context.class);
+ when(mockContext.getPackageName()).thenReturn(packageName);
+ when(mockContext.getUserId()).thenReturn(userId);
+ when(mockContext.getUser()).thenReturn(UserHandle.of(userId));
+ ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mockContext, id).setShortLabel(id);
+ return builder.build();
+ }
+
private static UsageEvents.Event createUsageEvent(String packageName) {
UsageEvents.Event e = new UsageEvents.Event();
e.mPackage = packageName;
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index 4dec7a1..a07e60c 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -112,6 +112,8 @@
private static final int UID_SYSTEM_HEADFULL = 10002;
private static final String PACKAGE_SYSTEM_HEADLESS = "com.example.system.headless";
private static final int UID_SYSTEM_HEADLESS = 10003;
+ private static final String PACKAGE_WELLBEING = "com.example.wellbeing";
+ private static final int UID_WELLBEING = 10004;
private static final int USER_ID = 0;
private static final int USER_ID2 = 10;
private static final UserHandle USER_HANDLE_USER2 = new UserHandle(USER_ID2);
@@ -218,6 +220,11 @@
}
@Override
+ boolean isWellbeingPackage(String packageName) {
+ return PACKAGE_WELLBEING.equals(packageName);
+ }
+
+ @Override
void updatePowerWhitelistCache() {
}
@@ -329,6 +336,12 @@
pish.packageName = PACKAGE_SYSTEM_HEADLESS;
packages.add(pish);
+ PackageInfo piw = new PackageInfo();
+ piw.applicationInfo = new ApplicationInfo();
+ piw.applicationInfo.uid = UID_WELLBEING;
+ piw.packageName = PACKAGE_WELLBEING;
+ packages.add(piw);
+
doReturn(packages).when(mockPm).getInstalledPackagesAsUser(anyInt(), anyInt());
try {
for (int i = 0; i < packages.size(); ++i) {
@@ -1516,6 +1529,25 @@
assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
}
+ @Test
+ public void testWellbeingAppElevated() {
+ reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_WELLBEING);
+ assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_WELLBEING);
+ reportEvent(mController, USER_INTERACTION, mInjector.mElapsedRealtime, PACKAGE_1);
+ assertBucket(STANDBY_BUCKET_ACTIVE, PACKAGE_1);
+ mInjector.mElapsedRealtime += RESTRICTED_THRESHOLD;
+
+ // Make sure the default wellbeing app does not get lowered below WORKING_SET.
+ mController.setAppStandbyBucket(PACKAGE_WELLBEING, USER_ID, STANDBY_BUCKET_RARE,
+ REASON_MAIN_TIMEOUT);
+ assertBucket(STANDBY_BUCKET_WORKING_SET, PACKAGE_WELLBEING);
+
+ // A non default wellbeing app should be able to fall lower than WORKING_SET.
+ mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE,
+ REASON_MAIN_TIMEOUT);
+ assertBucket(STANDBY_BUCKET_RARE, PACKAGE_1);
+ }
+
private String getAdminAppsStr(int userId) {
return getAdminAppsStr(userId, mController.getActiveAdminAppsForTest(userId));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 02a3bb1..a37f4be 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -548,7 +548,7 @@
final ActivityStack stack = new StackBuilder(mRootWindowContainer).build();
try {
doReturn(false).when(stack).isTranslucent(any());
- assertFalse(mStack.shouldBeVisible(null /* starting */));
+ assertTrue(mStack.shouldBeVisible(null /* starting */));
mActivity.setLastReportedConfiguration(new MergedConfiguration(new Configuration(),
mActivity.getConfiguration()));
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 6a84c13..c7a8bd8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1149,8 +1149,10 @@
verify(t, never()).setPosition(any(), eq(0), eq(0));
// Launch another activity before the transition is finished.
- final ActivityRecord app2 = new ActivityTestsBase.StackBuilder(mWm.mRoot)
- .setDisplay(mDisplayContent).build().getTopMostActivity();
+ final ActivityStack stack2 = new ActivityTestsBase.StackBuilder(mWm.mRoot)
+ .setDisplay(mDisplayContent).build();
+ final ActivityRecord app2 = new ActivityTestsBase.ActivityBuilder(mWm.mAtmService)
+ .setStack(stack2).setUseProcess(app.app).build();
app2.setVisible(false);
mDisplayContent.mOpeningApps.add(app2);
app2.setRequestedOrientation(newOrientation);
@@ -1160,6 +1162,12 @@
assertTrue(app.hasFixedRotationTransform(app2));
assertTrue(mDisplayContent.isFixedRotationLaunchingApp(app2));
+ final Configuration expectedProcConfig = new Configuration(app2.app.getConfiguration());
+ expectedProcConfig.windowConfiguration.setActivityType(
+ WindowConfiguration.ACTIVITY_TYPE_UNDEFINED);
+ assertEquals("The process should receive rotated configuration for compatibility",
+ expectedProcConfig, app2.app.getConfiguration());
+
// The fixed rotation transform can only be finished when all animation finished.
doReturn(false).when(app2).isAnimating(anyInt(), anyInt());
mDisplayContent.mAppTransition.notifyAppTransitionFinishedLocked(app2.token);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index 24950ce..a46e6d3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -266,6 +266,15 @@
mWpc.onMergedOverrideConfigurationChanged(config);
assertEquals(ACTIVITY_TYPE_HOME, config.windowConfiguration.getActivityType());
assertEquals(ACTIVITY_TYPE_UNDEFINED, mWpc.getActivityType());
+
+ final int globalSeq = 100;
+ mRootWindowContainer.getConfiguration().seq = globalSeq;
+ invertOrientation(mWpc.getConfiguration());
+ new ActivityBuilder(mService).setCreateTask(true).setUseProcess(mWpc).build();
+
+ assertTrue(mWpc.registeredForActivityConfigChanges());
+ assertEquals("Config seq of process should not be affected by activity",
+ mWpc.getConfiguration().seq, globalSeq);
}
private TestDisplayContent createTestDisplayContentInContainer() {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 8ae1ee9..3d51d7c 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -1888,8 +1888,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -1941,8 +1941,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -2010,8 +2010,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -2088,8 +2088,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -2126,8 +2126,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -2210,8 +2210,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -2247,8 +2247,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -3853,8 +3853,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -3891,8 +3891,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -4145,8 +4145,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.
@@ -4184,8 +4184,8 @@
*
* <p>Starting with API level 29, persistent device identifiers are guarded behind additional
* restrictions, and apps are recommended to use resettable identifiers (see <a
- * href="c"> Best practices for unique identifiers</a>). This method can be invoked if one of
- * the following requirements is met:
+ * href="/training/articles/user-data-ids">Best practices for unique identifiers</a>). This
+ * method can be invoked if one of the following requirements is met:
* <ul>
* <li>If the calling app has been granted the READ_PRIVILEGED_PHONE_STATE permission; this
* is a privileged permission that can only be granted to apps preloaded on the device.