Merge changes from topic "udfps_keyguard_view_controller_to_kotlin-master"
* changes:
1/N sideFPs bouncer, separate altBouncer callback
Refactor file from java => kotlin
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
deleted file mode 100644
index 78a77fe..0000000
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.job.controllers;
-
-import java.util.Objects;
-
-/** Wrapper class to represent a userId-pkgName combo. */
-final class Package {
- public final String packageName;
- public final int userId;
-
- Package(int userId, String packageName) {
- this.userId = userId;
- this.packageName = packageName;
- }
-
- @Override
- public String toString() {
- return packageToString(userId, packageName);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (!(obj instanceof Package)) {
- return false;
- }
- Package other = (Package) obj;
- return userId == other.userId && Objects.equals(packageName, other.packageName);
- }
-
- @Override
- public int hashCode() {
- return packageName.hashCode() + userId;
- }
-
- /**
- * Standardize the output of userId-packageName combo.
- */
- static String packageToString(int userId, String packageName) {
- return "<" + userId + ">" + packageName;
- }
-}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index d69b9e0..c46ffd7 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -21,7 +21,6 @@
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
import static com.android.server.job.JobSchedulerService.sSystemClock;
-import static com.android.server.job.controllers.Package.packageToString;
import android.annotation.CurrentTimeMillisLong;
import android.annotation.ElapsedRealtimeLong;
@@ -31,6 +30,7 @@
import android.app.usage.UsageStatsManagerInternal.EstimatedLaunchTimeChangedListener;
import android.appwidget.AppWidgetManager;
import android.content.Context;
+import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -172,7 +172,7 @@
final String pkgName = jobStatus.getSourcePackageName();
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
@@ -186,7 +186,7 @@
final int userId = UserHandle.getUserId(uid);
mTrackedJobs.delete(userId, packageName);
mEstimatedLaunchTimes.delete(userId, packageName);
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, packageName));
}
@Override
@@ -354,7 +354,7 @@
@CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs == null || jobs.size() == 0) {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
@@ -365,10 +365,10 @@
// Set alarm to be notified when this crosses the threshold.
final long timeToCrossThresholdMs =
nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
- mThresholdAlarmListener.addAlarm(new Package(userId, pkgName),
+ mThresholdAlarmListener.addAlarm(UserPackage.of(userId, pkgName),
nowElapsed + timeToCrossThresholdMs);
} else {
- mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+ mThresholdAlarmListener.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
@@ -427,25 +427,25 @@
}
/** Track when apps will cross the "will run soon" threshold. */
- private class ThresholdAlarmListener extends AlarmQueue<Package> {
+ private class ThresholdAlarmListener extends AlarmQueue<UserPackage> {
private ThresholdAlarmListener(Context context, Looper looper) {
super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
synchronized (mLock) {
final long now = sSystemClock.millis();
final long nowElapsed = sElapsedRealtimeClock.millis();
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
Slog.e(TAG, "Alarm expired for "
+ packageToString(p.userId, p.packageName) + " at the wrong time");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 659e7c0..d8206ad 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -28,7 +28,6 @@
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
-import static com.android.server.job.controllers.Package.packageToString;
import android.Manifest;
import android.annotation.NonNull;
@@ -44,6 +43,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserPackage;
import android.os.BatteryManager;
import android.os.Handler;
import android.os.Looper;
@@ -532,7 +532,7 @@
*/
private final SparseSetArray<String> mSystemInstallers = new SparseSetArray<>();
- /** An app has reached its quota. The message should contain a {@link Package} object. */
+ /** An app has reached its quota. The message should contain a {@link UserPackage} object. */
@VisibleForTesting
static final int MSG_REACHED_QUOTA = 0;
/** Drop any old timing sessions. */
@@ -542,7 +542,7 @@
/** Process state for a UID has changed. */
private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
/**
- * An app has reached its expedited job quota. The message should contain a {@link Package}
+ * An app has reached its expedited job quota. The message should contain a {@link UserPackage}
* object.
*/
@VisibleForTesting
@@ -680,7 +680,7 @@
final String pkgName = jobStatus.getSourcePackageName();
ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
}
}
@@ -746,7 +746,7 @@
}
mTimingEvents.delete(userId, packageName);
mEJTimingSessions.delete(userId, packageName);
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mExecutionStatsCache.delete(userId, packageName);
mEJStats.delete(userId, packageName);
mTopAppTrackers.delete(userId, packageName);
@@ -1725,7 +1725,7 @@
// exempted.
maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
} else {
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
}
return changedJobs;
}
@@ -1764,7 +1764,7 @@
&& isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
// TODO(141645789): we probably shouldn't cancel the alarm until we've verified
// that all jobs for the userId-package are within quota.
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
} else {
mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
}
@@ -1814,7 +1814,7 @@
if (jobs == null || jobs.size() == 0) {
Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
+ packageToString(userId, packageName) + " that has no jobs");
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
return;
}
@@ -1838,7 +1838,7 @@
+ getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ "ms in its quota.");
}
- mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
+ mInQuotaAlarmQueue.removeAlarmForKey(UserPackage.of(userId, packageName));
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
@@ -1903,7 +1903,7 @@
+ nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
}
- mInQuotaAlarmQueue.addAlarm(new Package(userId, packageName), inQuotaTimeElapsed);
+ mInQuotaAlarmQueue.addAlarm(UserPackage.of(userId, packageName), inQuotaTimeElapsed);
}
private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed,
@@ -2098,7 +2098,7 @@
}
private final class Timer {
- private final Package mPkg;
+ private final UserPackage mPkg;
private final int mUid;
private final boolean mRegularJobTimer;
@@ -2110,7 +2110,7 @@
private long mDebitAdjustment;
Timer(int uid, int userId, String packageName, boolean regularJobTimer) {
- mPkg = new Package(userId, packageName);
+ mPkg = UserPackage.of(userId, packageName);
mUid = uid;
mRegularJobTimer = regularJobTimer;
}
@@ -2365,7 +2365,7 @@
}
private final class TopAppTimer {
- private final Package mPkg;
+ private final UserPackage mPkg;
// List of jobs currently running for this app that started when the app wasn't in the
// foreground.
@@ -2373,7 +2373,7 @@
private long mStartTimeElapsed;
TopAppTimer(int userId, String packageName) {
- mPkg = new Package(userId, packageName);
+ mPkg = UserPackage.of(userId, packageName);
}
private int calculateTimeChunks(final long nowElapsed) {
@@ -2656,7 +2656,7 @@
synchronized (mLock) {
switch (msg.what) {
case MSG_REACHED_QUOTA: {
- Package pkg = (Package) msg.obj;
+ UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
}
@@ -2685,7 +2685,7 @@
break;
}
case MSG_REACHED_EJ_QUOTA: {
- Package pkg = (Package) msg.obj;
+ UserPackage pkg = (UserPackage) msg.obj;
if (DEBUG) {
Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota.");
}
@@ -2887,21 +2887,21 @@
}
/** Track when UPTCs are expected to come back into quota. */
- private class InQuotaAlarmQueue extends AlarmQueue<Package> {
+ private class InQuotaAlarmQueue extends AlarmQueue<UserPackage> {
private InQuotaAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false,
QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName)
.sendToTarget();
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 0eedcf0..44ac798 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -186,4 +186,11 @@
/** Dump any internal constants the Controller may have. */
public void dumpConstants(ProtoOutputStream proto) {
}
+
+ /**
+ * Standardize the output of userId-packageName combo.
+ */
+ static String packageToString(int userId, String packageName) {
+ return "<" + userId + ">" + packageName;
+ }
}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index 7a13e3f..abc196f 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -36,6 +36,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.UserPackage;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -824,7 +825,7 @@
void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
mScribe.discardLedgerLocked(userId, pkgName);
mCurrentOngoingEvents.delete(userId, pkgName);
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
}
@GuardedBy("mLock")
@@ -959,7 +960,7 @@
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null || mIrs.isVip(userId, pkgName)) {
// No ongoing transactions. No reason to schedule
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
mTrendCalculator.reset(getBalanceLocked(userId, pkgName),
@@ -972,7 +973,7 @@
if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
// Will never cross a threshold based on current events.
- mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
+ mBalanceThresholdAlarmQueue.removeAlarmForKey(UserPackage.of(userId, pkgName));
return;
}
timeToThresholdMs = upperTimeMs;
@@ -980,7 +981,7 @@
timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
}
- mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName),
+ mBalanceThresholdAlarmQueue.addAlarm(UserPackage.of(userId, pkgName),
SystemClock.elapsedRealtime() + timeToThresholdMs);
}
@@ -1071,57 +1072,22 @@
private final OngoingEventUpdater mOngoingEventUpdater = new OngoingEventUpdater();
- private static final class Package {
- public final String packageName;
- public final int userId;
-
- Package(int userId, String packageName) {
- this.userId = userId;
- this.packageName = packageName;
- }
-
- @Override
- public String toString() {
- return appToString(userId, packageName);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (this == obj) {
- return true;
- }
- if (obj instanceof Package) {
- Package other = (Package) obj;
- return userId == other.userId && Objects.equals(packageName, other.packageName);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return packageName.hashCode() + userId;
- }
- }
-
/** Track when apps will cross the closest affordability threshold (in both directions). */
- private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> {
+ private class BalanceThresholdAlarmQueue extends AlarmQueue<UserPackage> {
private BalanceThresholdAlarmQueue(Context context, Looper looper) {
super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
15_000L);
}
@Override
- protected boolean isForUser(@NonNull Package key, int userId) {
+ protected boolean isForUser(@NonNull UserPackage key, int userId) {
return key.userId == userId;
}
@Override
- protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+ protected void processExpiredAlarms(@NonNull ArraySet<UserPackage> expired) {
for (int i = 0; i < expired.size(); ++i) {
- Package p = expired.valueAt(i);
+ UserPackage p = expired.valueAt(i);
mHandler.obtainMessage(
MSG_CHECK_INDIVIDUAL_AFFORDABILITY, p.userId, 0, p.packageName)
.sendToTarget();
diff --git a/core/api/current.txt b/core/api/current.txt
index b42face..19b476a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -143,6 +143,7 @@
field public static final String READ_MEDIA_AUDIO = "android.permission.READ_MEDIA_AUDIO";
field public static final String READ_MEDIA_IMAGES = "android.permission.READ_MEDIA_IMAGES";
field public static final String READ_MEDIA_VIDEO = "android.permission.READ_MEDIA_VIDEO";
+ field public static final String READ_MEDIA_VISUAL_USER_SELECTED = "android.permission.READ_MEDIA_VISUAL_USER_SELECTED";
field public static final String READ_NEARBY_STREAMING_POLICY = "android.permission.READ_NEARBY_STREAMING_POLICY";
field public static final String READ_PHONE_NUMBERS = "android.permission.READ_PHONE_NUMBERS";
field public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
@@ -8955,9 +8956,18 @@
package android.companion {
+ public final class AssociatedDevice implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.bluetooth.le.ScanResult getBleDevice();
+ method @Nullable public android.bluetooth.BluetoothDevice getBluetoothDevice();
+ method @Nullable public android.net.wifi.ScanResult getWifiDevice();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.AssociatedDevice> CREATOR;
+ }
+
public final class AssociationInfo implements android.os.Parcelable {
method public int describeContents();
- method @Nullable public android.os.Parcelable getAssociatedDevice();
+ method @Nullable public android.companion.AssociatedDevice getAssociatedDevice();
method @Nullable public android.net.MacAddress getDeviceMacAddress();
method @Nullable public String getDeviceProfile();
method @Nullable public CharSequence getDisplayName();
@@ -41762,6 +41772,7 @@
field public static final String KEY_PING_TEST_BEFORE_DATA_SWITCH_BOOL = "ping_test_before_data_switch_bool";
field public static final String KEY_PREFER_2G_BOOL = "prefer_2g_bool";
field public static final String KEY_PREMIUM_CAPABILITY_MAXIMUM_NOTIFICATION_COUNT_INT_ARRAY = "premium_capability_maximum_notification_count_int_array";
+ field public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG = "premium_capability_network_setup_time_millis_long";
field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_notification_backoff_hysteresis_time_millis_long";
field public static final String KEY_PREMIUM_CAPABILITY_NOTIFICATION_DISPLAY_TIMEOUT_MILLIS_LONG = "premium_capability_notification_display_timeout_millis_long";
field public static final String KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG = "premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
@@ -44069,6 +44080,7 @@
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE = 12; // 0xc
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14; // 0xe
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_OVERRIDDEN = 5; // 0x5
+ field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15; // 0xf
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_REQUEST_FAILED = 11; // 0xb
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS = 1; // 0x1
field public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED = 2; // 0x2
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index dcbe7ed..352b4f9 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -587,6 +587,7 @@
field public static final String OPSTR_READ_MEDIA_AUDIO = "android:read_media_audio";
field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images";
field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video";
+ field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected";
field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio";
field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast";
field public static final String OPSTR_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO = "android:receive_explicit_user_interaction_audio";
@@ -15924,10 +15925,17 @@
package android.view.accessibility {
+ public abstract class AccessibilityDisplayProxy {
+ ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>);
+ method public int getDisplayId();
+ }
+
public final class AccessibilityManager {
method public int getAccessibilityWindowId(@Nullable android.os.IBinder);
method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void performAccessibilityShortcut();
+ method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean registerDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void registerSystemAction(@NonNull android.app.RemoteAction, int);
+ method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public boolean unregisterDisplayProxy(@NonNull android.view.accessibility.AccessibilityDisplayProxy);
method @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public void unregisterSystemAction(int);
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index cdb1510..e9f9136 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3159,7 +3159,7 @@
public final class InputMethodManager {
method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession();
method public int getDisplayId();
- method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);
+ method @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List<android.view.inputmethod.InputMethodInfo> getInputMethodListAsUser(int);
method public boolean hasActiveInputConnection(@Nullable android.view.View);
method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShown();
method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void setStylusWindowIdleTimeoutForTest(long);
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java
index a573776..b3db38d 100644
--- a/core/java/android/accounts/AccountManager.java
+++ b/core/java/android/accounts/AccountManager.java
@@ -27,7 +27,6 @@
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
-import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.PropertyInvalidatedCache;
import android.compat.annotation.UnsupportedAppUsage;
@@ -37,6 +36,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
+import android.content.pm.UserPackage;
import android.content.res.Resources;
import android.database.SQLException;
import android.os.Build;
@@ -348,43 +348,11 @@
*/
public static final int CACHE_ACCOUNTS_DATA_SIZE = 4;
- private static final class UserIdPackage
- {
- @UserIdInt
- public int userId;
- public String packageName;
-
- public UserIdPackage(int UserId, String PackageName) {
- this.userId = UserId;
- this.packageName = PackageName;
- }
-
- @Override
- public boolean equals(@Nullable Object o) {
- if (o == null) {
- return false;
- }
- if (o == this) {
- return true;
- }
- if (o.getClass() != getClass()) {
- return false;
- }
- UserIdPackage e = (UserIdPackage) o;
- return e.userId == userId && e.packageName.equals(packageName);
- }
-
- @Override
- public int hashCode() {
- return userId ^ packageName.hashCode();
- }
- }
-
- PropertyInvalidatedCache<UserIdPackage, Account[]> mAccountsForUserCache =
- new PropertyInvalidatedCache<UserIdPackage, Account[]>(
+ PropertyInvalidatedCache<UserPackage, Account[]> mAccountsForUserCache =
+ new PropertyInvalidatedCache<UserPackage, Account[]>(
CACHE_ACCOUNTS_DATA_SIZE, CACHE_KEY_ACCOUNTS_DATA_PROPERTY) {
@Override
- public Account[] recompute(UserIdPackage userAndPackage) {
+ public Account[] recompute(UserPackage userAndPackage) {
try {
return mService.getAccountsAsUser(null, userAndPackage.userId, userAndPackage.packageName);
} catch (RemoteException e) {
@@ -392,7 +360,7 @@
}
}
@Override
- public boolean bypass(UserIdPackage query) {
+ public boolean bypass(UserPackage query) {
return query.userId < 0;
}
@Override
@@ -731,7 +699,7 @@
*/
@NonNull
public Account[] getAccountsAsUser(int userId) {
- UserIdPackage userAndPackage = new UserIdPackage(userId, mContext.getOpPackageName());
+ UserPackage userAndPackage = UserPackage.of(userId, mContext.getOpPackageName());
return mAccountsForUserCache.query(userAndPackage);
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 1b972e0..267e5b6 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1360,9 +1360,17 @@
*/
public static final int OP_RUN_LONG_JOBS = AppProtoEnums.APP_OP_RUN_LONG_JOBS;
+ /**
+ * Notify apps that they have been granted URI permission photos
+ *
+ * @hide
+ */
+ public static final int OP_READ_MEDIA_VISUAL_USER_SELECTED =
+ AppProtoEnums.APP_OP_READ_MEDIA_VISUAL_USER_SELECTED;
+
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 123;
+ public static final int _NUM_OP = 124;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1833,6 +1841,14 @@
@SystemApi
public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO =
"android:receive_ambient_trigger_audio";
+ /**
+ * Notify apps that they have been granted URI permission photos
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED =
+ "android:read_media_visual_user_selected";
/**
* Record audio from near-field microphone (ie. TV remote)
@@ -1948,6 +1964,7 @@
OP_MANAGE_MEDIA,
OP_TURN_SCREEN_ON,
OP_RUN_LONG_JOBS,
+ OP_READ_MEDIA_VISUAL_USER_SELECTED,
};
static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2329,7 +2346,11 @@
"RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO").setDefaultMode(
AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_RUN_LONG_JOBS, OPSTR_RUN_LONG_JOBS, "RUN_LONG_JOBS")
- .setPermission(Manifest.permission.RUN_LONG_JOBS).build()
+ .setPermission(Manifest.permission.RUN_LONG_JOBS).build(),
+ new AppOpInfo.Builder(OP_READ_MEDIA_VISUAL_USER_SELECTED,
+ OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, "READ_MEDIA_VISUAL_USER_SELECTED")
+ .setPermission(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
+ .setDefaultMode(AppOpsManager.MODE_ALLOWED).build()
};
/**
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index a2dc47d..5a2f261 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -458,7 +458,8 @@
&& isVisible == that.isVisible
&& isSleeping == that.isSleeping
&& Objects.equals(mTopActivityLocusId, that.mTopActivityLocusId)
- && parentTaskId == that.parentTaskId;
+ && parentTaskId == that.parentTaskId
+ && Objects.equals(topActivity, that.topActivity);
}
/**
diff --git a/core/java/android/companion/AssociatedDevice.java b/core/java/android/companion/AssociatedDevice.java
index 3758cdb..a833661 100644
--- a/core/java/android/companion/AssociatedDevice.java
+++ b/core/java/android/companion/AssociatedDevice.java
@@ -16,6 +16,7 @@
package android.companion;
+import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,19 +24,14 @@
import androidx.annotation.Nullable;
/**
- * Loose wrapper around device parcelable. Device can be one of three types:
+ * Container for device info from an association that is not self-managed.
+ * Device can be one of three types:
*
* <ul>
* <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li>
* <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li>
* <li>for WiFi - {@link android.net.wifi.ScanResult}</li>
* </ul>
- *
- * This class serves as temporary wrapper to deliver a loosely-typed parcelable object from
- * {@link com.android.companiondevicemanager.CompanionDeviceActivity} to the Companion app,
- * and should only be used internally.
- *
- * @hide
*/
public final class AssociatedDevice implements Parcelable {
private static final int CLASSIC_BLUETOOTH = 0;
@@ -44,6 +40,7 @@
@NonNull private final Parcelable mDevice;
+ /** @hide */
public AssociatedDevice(@NonNull Parcelable device) {
mDevice = device;
}
@@ -54,11 +51,39 @@
}
/**
- * Return device info. Cast to expected device type.
+ * Return bluetooth device info. Null if associated device is not a bluetooth device.
+ * @return Remote bluetooth device details containing MAC address.
*/
- @NonNull
- public Parcelable getDevice() {
- return mDevice;
+ @Nullable
+ public BluetoothDevice getBluetoothDevice() {
+ if (mDevice instanceof BluetoothDevice) {
+ return (BluetoothDevice) mDevice;
+ }
+ return null;
+ }
+
+ /**
+ * Return bluetooth LE device info. Null if associated device is not a BLE device.
+ * @return BLE scan result containing details of detected BLE device.
+ */
+ @Nullable
+ public android.bluetooth.le.ScanResult getBleDevice() {
+ if (mDevice instanceof android.bluetooth.le.ScanResult) {
+ return (android.bluetooth.le.ScanResult) mDevice;
+ }
+ return null;
+ }
+
+ /**
+ * Return Wi-Fi device info. Null if associated device is not a Wi-Fi device.
+ * @return Wi-Fi scan result containing details of detected access point.
+ */
+ @Nullable
+ public android.net.wifi.ScanResult getWifiDevice() {
+ if (mDevice instanceof android.net.wifi.ScanResult) {
+ return (android.net.wifi.ScanResult) mDevice;
+ }
+ return null;
}
@Override
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index 93964b3..5fd39fe 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -164,20 +164,19 @@
/**
* Companion device that was associated. Note that this field is not persisted across sessions.
- *
- * Cast to expected device type before use:
+ * Device can be one of the following types:
*
* <ul>
- * <li>for classic Bluetooth - {@link android.bluetooth.BluetoothDevice}</li>
- * <li>for Bluetooth LE - {@link android.bluetooth.le.ScanResult}</li>
- * <li>for WiFi - {@link android.net.wifi.ScanResult}</li>
+ * <li>for classic Bluetooth - {@link AssociatedDevice#getBluetoothDevice()}</li>
+ * <li>for Bluetooth LE - {@link AssociatedDevice#getBleDevice()}</li>
+ * <li>for WiFi - {@link AssociatedDevice#getWifiDevice()}</li>
* </ul>
*
* @return the companion device that was associated, or {@code null} if the device is
- * self-managed.
+ * self-managed or this association info was retrieved from persistent storage.
*/
- public @Nullable Parcelable getAssociatedDevice() {
- return mAssociatedDevice == null ? null : mAssociatedDevice.getDevice();
+ public @Nullable AssociatedDevice getAssociatedDevice() {
+ return mAssociatedDevice;
}
/**
diff --git a/core/java/android/content/pm/UserPackage.java b/core/java/android/content/pm/UserPackage.java
new file mode 100644
index 0000000..e75f551
--- /dev/null
+++ b/core/java/android/content/pm/UserPackage.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.SparseArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Objects;
+
+/**
+ * POJO to represent a package for a specific user ID.
+ *
+ * @hide
+ */
+public final class UserPackage {
+ @UserIdInt
+ public final int userId;
+ public final String packageName;
+
+ @GuardedBy("sCache")
+ private static final SparseArrayMap<String, UserPackage> sCache = new SparseArrayMap<>();
+
+ private static final Object sUserIdLock = new Object();
+ private static final class NoPreloadHolder {
+ /** Set of userIDs to cache objects for. */
+ @GuardedBy("sUserIdLock")
+ private static int[] sUserIds = new int[]{UserHandle.getUserId(Process.myUid())};
+ }
+
+ private UserPackage(int userId, String packageName) {
+ this.userId = userId;
+ this.packageName = packageName;
+ }
+
+ @Override
+ public String toString() {
+ return "<" + userId + ">" + packageName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof UserPackage) {
+ UserPackage other = (UserPackage) obj;
+ return userId == other.userId && Objects.equals(packageName, other.packageName);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = 31 * result + userId;
+ result = 31 * result + packageName.hashCode();
+ return result;
+ }
+
+ /** Return an instance of this class representing the given userId + packageName combination. */
+ @NonNull
+ public static UserPackage of(@UserIdInt int userId, @NonNull String packageName) {
+ synchronized (sUserIdLock) {
+ if (!ArrayUtils.contains(NoPreloadHolder.sUserIds, userId)) {
+ // Don't cache objects for invalid userIds.
+ return new UserPackage(userId, packageName);
+ }
+ }
+ synchronized (sCache) {
+ UserPackage up = sCache.get(userId, packageName);
+ if (up == null) {
+ packageName = packageName.intern();
+ up = new UserPackage(userId, packageName);
+ sCache.add(userId, packageName, up);
+ }
+ return up;
+ }
+ }
+
+ /** Remove the specified app from the cache. */
+ public static void removeFromCache(@UserIdInt int userId, @NonNull String packageName) {
+ synchronized (sCache) {
+ sCache.delete(userId, packageName);
+ }
+ }
+
+ /** Indicate the list of valid user IDs on the device. */
+ public static void setValidUserIds(@NonNull int[] userIds) {
+ userIds = userIds.clone();
+ synchronized (sUserIdLock) {
+ NoPreloadHolder.sUserIds = userIds;
+ }
+ synchronized (sCache) {
+ for (int u = sCache.numMaps() - 1; u >= 0; --u) {
+ final int userId = sCache.keyAt(u);
+ // Not holding sUserIdLock is intentional here. We don't modify the elements within
+ // the array and so even if this method is called multiple times with different sets
+ // of user IDs, we want to adjust the cache based on each new array.
+ if (!ArrayUtils.contains(userIds, userId)) {
+ sCache.deleteAt(u);
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java
new file mode 100644
index 0000000..9cc9c72
--- /dev/null
+++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Per-provider metadata and entries for the create-credential flow.
+ *
+ * @hide
+ */
+public class CreateCredentialProviderData extends ProviderData implements Parcelable {
+ @NonNull
+ private final List<Entry> mSaveEntries;
+ @NonNull
+ private final List<Entry> mActionChips;
+ private final boolean mIsDefaultProvider;
+ @Nullable
+ private final Entry mRemoteEntry;
+
+ public CreateCredentialProviderData(
+ @NonNull String providerFlattenedComponentName, @NonNull List<Entry> saveEntries,
+ @NonNull List<Entry> actionChips, boolean isDefaultProvider,
+ @Nullable Entry remoteEntry) {
+ super(providerFlattenedComponentName);
+ mSaveEntries = saveEntries;
+ mActionChips = actionChips;
+ mIsDefaultProvider = isDefaultProvider;
+ mRemoteEntry = remoteEntry;
+ }
+
+ @NonNull
+ public List<Entry> getSaveEntries() {
+ return mSaveEntries;
+ }
+
+ @NonNull
+ public List<Entry> getActionChips() {
+ return mActionChips;
+ }
+
+ public boolean isDefaultProvider() {
+ return mIsDefaultProvider;
+ }
+
+ @Nullable
+ public Entry getRemoteEntry() {
+ return mRemoteEntry;
+ }
+
+ protected CreateCredentialProviderData(@NonNull Parcel in) {
+ super(in);
+
+ List<Entry> credentialEntries = new ArrayList<>();
+ in.readTypedList(credentialEntries, Entry.CREATOR);
+ mSaveEntries = credentialEntries;
+ AnnotationValidations.validate(NonNull.class, null, mSaveEntries);
+
+ List<Entry> actionChips = new ArrayList<>();
+ in.readTypedList(actionChips, Entry.CREATOR);
+ mActionChips = actionChips;
+ AnnotationValidations.validate(NonNull.class, null, mActionChips);
+
+ mIsDefaultProvider = in.readBoolean();
+
+ Entry remoteEntry = in.readTypedObject(Entry.CREATOR);
+ mRemoteEntry = remoteEntry;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeTypedList(mSaveEntries);
+ dest.writeTypedList(mActionChips);
+ dest.writeBoolean(isDefaultProvider());
+ dest.writeTypedObject(mRemoteEntry, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<CreateCredentialProviderData> CREATOR =
+ new Creator<CreateCredentialProviderData>() {
+ @Override
+ public CreateCredentialProviderData createFromParcel(@NonNull Parcel in) {
+ return new CreateCredentialProviderData(in);
+ }
+
+ @Override
+ public CreateCredentialProviderData[] newArray(int size) {
+ return new CreateCredentialProviderData[size];
+ }
+ };
+
+ /**
+ * Builder for {@link CreateCredentialProviderData}.
+ *
+ * @hide
+ */
+ public static class Builder {
+ private @NonNull String mProviderFlattenedComponentName;
+ private @NonNull List<Entry> mSaveEntries = new ArrayList<>();
+ private @NonNull List<Entry> mActionChips = new ArrayList<>();
+ private boolean mIsDefaultProvider = false;
+ private @Nullable Entry mRemoteEntry = null;
+
+ /** Constructor with required properties. */
+ public Builder(@NonNull String providerFlattenedComponentName) {
+ mProviderFlattenedComponentName = providerFlattenedComponentName;
+ }
+
+ /** Sets the list of save credential entries to be displayed to the user. */
+ @NonNull
+ public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) {
+ mSaveEntries = credentialEntries;
+ return this;
+ }
+
+ /** Sets the list of action chips to be displayed to the user. */
+ @NonNull
+ public Builder setActionChips(@NonNull List<Entry> actionChips) {
+ mActionChips = actionChips;
+ return this;
+ }
+
+ /** Sets whether this provider is the user's selected default provider. */
+ @NonNull
+ public Builder setIsDefaultProvider(boolean isDefaultProvider) {
+ mIsDefaultProvider = isDefaultProvider;
+ return this;
+ }
+
+ /** Builds a {@link CreateCredentialProviderData}. */
+ @NonNull
+ public CreateCredentialProviderData build() {
+ return new CreateCredentialProviderData(mProviderFlattenedComponentName,
+ mSaveEntries, mActionChips, mIsDefaultProvider, mRemoteEntry);
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java
new file mode 100644
index 0000000..73c8dbe
--- /dev/null
+++ b/core/java/android/credentials/ui/DisabledProviderData.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Metadata of a disabled provider.
+ *
+ * @hide
+ */
+public class DisabledProviderData extends ProviderData implements Parcelable {
+
+ public DisabledProviderData(
+ @NonNull String providerFlattenedComponentName) {
+ super(providerFlattenedComponentName);
+ }
+
+ protected DisabledProviderData(@NonNull Parcel in) {
+ super(in);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<DisabledProviderData> CREATOR = new Creator<>() {
+ @Override
+ public DisabledProviderData createFromParcel(@NonNull Parcel in) {
+ return new DisabledProviderData(in);
+ }
+
+ @Override
+ public DisabledProviderData[] newArray(int size) {
+ return new DisabledProviderData[size];
+ }
+ };
+}
diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java
new file mode 100644
index 0000000..834f9825
--- /dev/null
+++ b/core/java/android/credentials/ui/GetCredentialProviderData.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.AnnotationValidations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Per-provider metadata and entries for the get-credential flow.
+ *
+ * @hide
+ */
+public class GetCredentialProviderData extends ProviderData implements Parcelable {
+ @NonNull
+ private final List<Entry> mCredentialEntries;
+ @NonNull
+ private final List<Entry> mActionChips;
+ @Nullable
+ private final Entry mAuthenticationEntry;
+ @Nullable
+ private final Entry mRemoteEntry;
+
+ public GetCredentialProviderData(
+ @NonNull String providerFlattenedComponentName, @NonNull List<Entry> credentialEntries,
+ @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
+ @Nullable Entry remoteEntry) {
+ super(providerFlattenedComponentName);
+ mCredentialEntries = credentialEntries;
+ mActionChips = actionChips;
+ mAuthenticationEntry = authenticationEntry;
+ mRemoteEntry = remoteEntry;
+ }
+
+ @NonNull
+ public List<Entry> getCredentialEntries() {
+ return mCredentialEntries;
+ }
+
+ @NonNull
+ public List<Entry> getActionChips() {
+ return mActionChips;
+ }
+
+ @Nullable
+ public Entry getAuthenticationEntry() {
+ return mAuthenticationEntry;
+ }
+
+ @Nullable
+ public Entry getRemoteEntry() {
+ return mRemoteEntry;
+ }
+
+ protected GetCredentialProviderData(@NonNull Parcel in) {
+ super(in);
+
+ List<Entry> credentialEntries = new ArrayList<>();
+ in.readTypedList(credentialEntries, Entry.CREATOR);
+ mCredentialEntries = credentialEntries;
+ AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
+
+ List<Entry> actionChips = new ArrayList<>();
+ in.readTypedList(actionChips, Entry.CREATOR);
+ mActionChips = actionChips;
+ AnnotationValidations.validate(NonNull.class, null, mActionChips);
+
+ Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
+ mAuthenticationEntry = authenticationEntry;
+
+ Entry remoteEntry = in.readTypedObject(Entry.CREATOR);
+ mRemoteEntry = remoteEntry;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeTypedList(mCredentialEntries);
+ dest.writeTypedList(mActionChips);
+ dest.writeTypedObject(mAuthenticationEntry, flags);
+ dest.writeTypedObject(mRemoteEntry, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<GetCredentialProviderData> CREATOR =
+ new Creator<GetCredentialProviderData>() {
+ @Override
+ public GetCredentialProviderData createFromParcel(@NonNull Parcel in) {
+ return new GetCredentialProviderData(in);
+ }
+
+ @Override
+ public GetCredentialProviderData[] newArray(int size) {
+ return new GetCredentialProviderData[size];
+ }
+ };
+
+ /**
+ * Builder for {@link GetCredentialProviderData}.
+ *
+ * @hide
+ */
+ public static class Builder {
+ private @NonNull String mProviderFlattenedComponentName;
+ private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
+ private @NonNull List<Entry> mActionChips = new ArrayList<>();
+ private @Nullable Entry mAuthenticationEntry = null;
+ private @Nullable Entry mRemoteEntry = null;
+
+ /** Constructor with required properties. */
+ public Builder(@NonNull String providerFlattenedComponentName) {
+ mProviderFlattenedComponentName = providerFlattenedComponentName;
+ }
+
+ /** Sets the list of save / get credential entries to be displayed to the user. */
+ @NonNull
+ public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+ mCredentialEntries = credentialEntries;
+ return this;
+ }
+
+ /** Sets the list of action chips to be displayed to the user. */
+ @NonNull
+ public Builder setActionChips(@NonNull List<Entry> actionChips) {
+ mActionChips = actionChips;
+ return this;
+ }
+
+ /** Sets the authentication entry to be displayed to the user. */
+ @NonNull
+ public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
+ mAuthenticationEntry = authenticationEntry;
+ return this;
+ }
+
+ /** Sets the remote entry to be displayed to the user. */
+ @NonNull
+ public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+ mRemoteEntry = remoteEntry;
+ return this;
+ }
+
+ /** Builds a {@link GetCredentialProviderData}. */
+ @NonNull
+ public GetCredentialProviderData build() {
+ return new GetCredentialProviderData(mProviderFlattenedComponentName,
+ mCredentialEntries, mActionChips, mAuthenticationEntry, mRemoteEntry);
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
index 1b70ea4..4751696 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -30,15 +30,20 @@
*/
public class IntentFactory {
/** Generate a new launch intent to the . */
- public static Intent newIntent(RequestInfo requestInfo,
- ArrayList<ProviderData> providerDataList, ResultReceiver resultReceiver) {
+ public static Intent newIntent(
+ RequestInfo requestInfo,
+ ArrayList<ProviderData> enabledProviderDataList,
+ ArrayList<DisabledProviderData> disabledProviderDataList,
+ ResultReceiver resultReceiver) {
Intent intent = new Intent();
// TODO: define these as proper config strings.
String activityName = "com.android.credentialmanager/.CredentialSelectorActivity";
intent.setComponent(ComponentName.unflattenFromString(activityName));
intent.putParcelableArrayListExtra(
- ProviderData.EXTRA_PROVIDER_DATA_LIST, providerDataList);
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
+ intent.putParcelableArrayListExtra(
+ ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
intent.putExtra(Constants.EXTRA_RESULT_RECEIVER,
toIpcFriendlyResultReceiver(resultReceiver));
diff --git a/core/java/android/credentials/ui/ProviderData.java b/core/java/android/credentials/ui/ProviderData.java
index 3728469..eeaeb46 100644
--- a/core/java/android/credentials/ui/ProviderData.java
+++ b/core/java/android/credentials/ui/ProviderData.java
@@ -16,232 +16,62 @@
package android.credentials.ui;
-import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.drawable.Icon;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.AnnotationValidations;
-import java.util.ArrayList;
-import java.util.List;
-
/**
- * Holds metadata and credential entries for a single provider.
+ * Super class for data structures that hold metadata and credential entries for a single provider.
*
* @hide
*/
-public class ProviderData implements Parcelable {
+public abstract class ProviderData implements Parcelable {
/**
- * The intent extra key for the list of {@code ProviderData} when launching the UX
- * activities.
+ * The intent extra key for the list of {@code ProviderData} from active providers when
+ * launching the UX activities.
*/
- public static final String EXTRA_PROVIDER_DATA_LIST =
- "android.credentials.ui.extra.PROVIDER_DATA_LIST";
+ public static final String EXTRA_ENABLED_PROVIDER_DATA_LIST =
+ "android.credentials.ui.extra.ENABLED_PROVIDER_DATA_LIST";
+ /**
+ * The intent extra key for the list of {@code ProviderData} from disabled providers when
+ * launching the UX activities.
+ */
+ public static final String EXTRA_DISABLED_PROVIDER_DATA_LIST =
+ "android.credentials.ui.extra.DISABLED_PROVIDER_DATA_LIST";
@NonNull
private final String mProviderFlattenedComponentName;
- @NonNull
- private final String mProviderDisplayName;
- @Nullable
- private final Icon mIcon;
- @NonNull
- private final List<Entry> mCredentialEntries;
- @NonNull
- private final List<Entry> mActionChips;
- @Nullable
- private final Entry mAuthenticationEntry;
-
- private final @CurrentTimeMillisLong long mLastUsedTimeMillis;
public ProviderData(
- @NonNull String providerFlattenedComponentName, @NonNull String providerDisplayName,
- @Nullable Icon icon, @NonNull List<Entry> credentialEntries,
- @NonNull List<Entry> actionChips, @Nullable Entry authenticationEntry,
- @CurrentTimeMillisLong long lastUsedTimeMillis) {
+ @NonNull String providerFlattenedComponentName) {
mProviderFlattenedComponentName = providerFlattenedComponentName;
- mProviderDisplayName = providerDisplayName;
- mIcon = icon;
- mCredentialEntries = credentialEntries;
- mActionChips = actionChips;
- mAuthenticationEntry = authenticationEntry;
- mLastUsedTimeMillis = lastUsedTimeMillis;
}
- /** Returns the unique provider id. */
+ /**
+ * Returns provider component name.
+ * It also serves as the unique identifier for this provider.
+ */
@NonNull
public String getProviderFlattenedComponentName() {
return mProviderFlattenedComponentName;
}
- @NonNull
- public String getProviderDisplayName() {
- return mProviderDisplayName;
- }
-
- @Nullable
- public Icon getIcon() {
- return mIcon;
- }
-
- @NonNull
- public List<Entry> getCredentialEntries() {
- return mCredentialEntries;
- }
-
- @NonNull
- public List<Entry> getActionChips() {
- return mActionChips;
- }
-
- @Nullable
- public Entry getAuthenticationEntry() {
- return mAuthenticationEntry;
- }
-
- /** Returns the time when the provider was last used. */
- public @CurrentTimeMillisLong long getLastUsedTimeMillis() {
- return mLastUsedTimeMillis;
- }
-
protected ProviderData(@NonNull Parcel in) {
String providerFlattenedComponentName = in.readString8();
mProviderFlattenedComponentName = providerFlattenedComponentName;
AnnotationValidations.validate(NonNull.class, null, mProviderFlattenedComponentName);
-
- String providerDisplayName = in.readString8();
- mProviderDisplayName = providerDisplayName;
- AnnotationValidations.validate(NonNull.class, null, mProviderDisplayName);
-
- Icon icon = in.readTypedObject(Icon.CREATOR);
- mIcon = icon;
-
- List<Entry> credentialEntries = new ArrayList<>();
- in.readTypedList(credentialEntries, Entry.CREATOR);
- mCredentialEntries = credentialEntries;
- AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
-
- List<Entry> actionChips = new ArrayList<>();
- in.readTypedList(actionChips, Entry.CREATOR);
- mActionChips = actionChips;
- AnnotationValidations.validate(NonNull.class, null, mActionChips);
-
- Entry authenticationEntry = in.readTypedObject(Entry.CREATOR);
- mAuthenticationEntry = authenticationEntry;
-
- long lastUsedTimeMillis = in.readLong();
- mLastUsedTimeMillis = lastUsedTimeMillis;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mProviderFlattenedComponentName);
- dest.writeString8(mProviderDisplayName);
- dest.writeTypedObject(mIcon, flags);
- dest.writeTypedList(mCredentialEntries);
- dest.writeTypedList(mActionChips);
- dest.writeTypedObject(mAuthenticationEntry, flags);
- dest.writeLong(mLastUsedTimeMillis);
}
@Override
public int describeContents() {
return 0;
}
-
- public static final @NonNull Creator<ProviderData> CREATOR = new Creator<ProviderData>() {
- @Override
- public ProviderData createFromParcel(@NonNull Parcel in) {
- return new ProviderData(in);
- }
-
- @Override
- public ProviderData[] newArray(int size) {
- return new ProviderData[size];
- }
- };
-
- /**
- * Builder for {@link ProviderData}.
- *
- * @hide
- */
- public static class Builder {
- private @NonNull String mProviderFlattenedComponentName;
- private @NonNull String mProviderDisplayName;
- private @Nullable Icon mIcon;
- private @NonNull List<Entry> mCredentialEntries = new ArrayList<>();
- private @NonNull List<Entry> mActionChips = new ArrayList<>();
- private @Nullable Entry mAuthenticationEntry = null;
- private @CurrentTimeMillisLong long mLastUsedTimeMillis = 0L;
-
- /** Constructor with required properties. */
- public Builder(@NonNull String providerFlattenedComponentName,
- @NonNull String providerDisplayName,
- @Nullable Icon icon) {
- mProviderFlattenedComponentName = providerFlattenedComponentName;
- mProviderDisplayName = providerDisplayName;
- mIcon = icon;
- }
-
- /** Sets the unique provider id. */
- @NonNull
- public Builder setProviderFlattenedComponentName(@NonNull String providerFlattenedComponentName) {
- mProviderFlattenedComponentName = providerFlattenedComponentName;
- return this;
- }
-
- /** Sets the provider display name to be displayed to the user. */
- @NonNull
- public Builder setProviderDisplayName(@NonNull String providerDisplayName) {
- mProviderDisplayName = providerDisplayName;
- return this;
- }
-
- /** Sets the provider icon to be displayed to the user. */
- @NonNull
- public Builder setIcon(@NonNull Icon icon) {
- mIcon = icon;
- return this;
- }
-
- /** Sets the list of save / get credential entries to be displayed to the user. */
- @NonNull
- public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
- mCredentialEntries = credentialEntries;
- return this;
- }
-
- /** Sets the list of action chips to be displayed to the user. */
- @NonNull
- public Builder setActionChips(@NonNull List<Entry> actionChips) {
- mActionChips = actionChips;
- return this;
- }
-
- /** Sets the authentication entry to be displayed to the user. */
- @NonNull
- public Builder setAuthenticationEntry(@Nullable Entry authenticationEntry) {
- mAuthenticationEntry = authenticationEntry;
- return this;
- }
-
- /** Sets the time when the provider was last used. */
- @NonNull
- public Builder setLastUsedTimeMillis(@CurrentTimeMillisLong long lastUsedTimeMillis) {
- mLastUsedTimeMillis = lastUsedTimeMillis;
- return this;
- }
-
- /** Builds a {@link ProviderData}. */
- @NonNull
- public ProviderData build() {
- return new ProviderData(mProviderFlattenedComponentName, mProviderDisplayName,
- mIcon, mCredentialEntries,
- mActionChips, mAuthenticationEntry, mLastUsedTimeMillis);
- }
- }
}
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index 619b08e..59d5118 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -69,6 +69,7 @@
private final boolean mIsFirstUsage;
+ // TODO: change to package name
@NonNull
private final String mAppDisplayName;
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 36ac1a0..8a92135 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -533,7 +533,6 @@
mProgramType = in.readInt();
mPrimaryId = in.readTypedObject(Identifier.CREATOR);
mSecondaryIds = in.createTypedArray(Identifier.CREATOR);
- Arrays.sort(mSecondaryIds);
if (Stream.of(mSecondaryIds).anyMatch(id -> id == null)) {
throw new IllegalArgumentException("secondaryIds list must not contain nulls");
}
diff --git a/core/java/android/service/autofill/FillRequest.java b/core/java/android/service/autofill/FillRequest.java
index b4010a4..0f7c9b6 100644
--- a/core/java/android/service/autofill/FillRequest.java
+++ b/core/java/android/service/autofill/FillRequest.java
@@ -111,6 +111,12 @@
*/
public static final @RequestFlags int FLAG_IME_SHOWING = 0x80;
+ /**
+ * Indicates whether autofill session should reset the fill dialog state.
+ * @hide
+ */
+ public static final @RequestFlags int FLAG_RESET_FILL_DIALOG_STATE = 0x100;
+
/** @hide */
public static final int INVALID_REQUEST_ID = Integer.MIN_VALUE;
@@ -208,7 +214,8 @@
FLAG_PASSWORD_INPUT_TYPE,
FLAG_VIEW_NOT_FOCUSED,
FLAG_SUPPORTS_FILL_DIALOG,
- FLAG_IME_SHOWING
+ FLAG_IME_SHOWING,
+ FLAG_RESET_FILL_DIALOG_STATE
})
@Retention(RetentionPolicy.SOURCE)
@DataClass.Generated.Member
@@ -236,6 +243,8 @@
return "FLAG_SUPPORTS_FILL_DIALOG";
case FLAG_IME_SHOWING:
return "FLAG_IME_SHOWING";
+ case FLAG_RESET_FILL_DIALOG_STATE:
+ return "FLAG_RESET_FILL_DIALOG_STATE";
default: return Integer.toHexString(value);
}
}
@@ -312,7 +321,8 @@
| FLAG_PASSWORD_INPUT_TYPE
| FLAG_VIEW_NOT_FOCUSED
| FLAG_SUPPORTS_FILL_DIALOG
- | FLAG_IME_SHOWING);
+ | FLAG_IME_SHOWING
+ | FLAG_RESET_FILL_DIALOG_STATE);
this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
this.mDelayedFillIntentSender = delayedFillIntentSender;
@@ -473,7 +483,8 @@
| FLAG_PASSWORD_INPUT_TYPE
| FLAG_VIEW_NOT_FOCUSED
| FLAG_SUPPORTS_FILL_DIALOG
- | FLAG_IME_SHOWING);
+ | FLAG_IME_SHOWING
+ | FLAG_RESET_FILL_DIALOG_STATE);
this.mInlineSuggestionsRequest = inlineSuggestionsRequest;
this.mDelayedFillIntentSender = delayedFillIntentSender;
@@ -495,10 +506,10 @@
};
@DataClass.Generated(
- time = 1647856966565L,
+ time = 1663290803064L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/service/autofill/FillRequest.java",
- inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
+ inputSignatures = "public static final @android.service.autofill.FillRequest.RequestFlags int FLAG_MANUAL_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_COMPATIBILITY_MODE_REQUEST\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_PASSWORD_INPUT_TYPE\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_VIEW_NOT_FOCUSED\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_SUPPORTS_FILL_DIALOG\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_IME_SHOWING\npublic static final @android.service.autofill.FillRequest.RequestFlags int FLAG_RESET_FILL_DIALOG_STATE\npublic static final int INVALID_REQUEST_ID\nprivate final int mId\nprivate final @android.annotation.NonNull java.util.List<android.service.autofill.FillContext> mFillContexts\nprivate final @android.annotation.Nullable android.os.Bundle mClientState\nprivate final @android.service.autofill.FillRequest.RequestFlags int mFlags\nprivate final @android.annotation.Nullable android.view.inputmethod.InlineSuggestionsRequest mInlineSuggestionsRequest\nprivate final @android.annotation.Nullable android.content.IntentSender mDelayedFillIntentSender\nprivate void onConstructed()\nclass FillRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genToString=true, genHiddenConstructor=true, genHiddenConstDefs=true)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/view/HandwritingDelegateConfiguration.java b/core/java/android/view/HandwritingDelegateConfiguration.java
index 524bb4c..719c614 100644
--- a/core/java/android/view/HandwritingDelegateConfiguration.java
+++ b/core/java/android/view/HandwritingDelegateConfiguration.java
@@ -47,7 +47,7 @@
* @param delegatorViewId identifier of the delegator editor view for which handwriting mode
* should be initiated
* @param initiationCallback callback called when a stylus {@link MotionEvent} occurs within
- * this view's bounds
+ * this view's bounds. This will be called from the UI thread.
*/
public HandwritingDelegateConfiguration(
@IdRes int delegatorViewId, @NonNull Runnable initiationCallback) {
@@ -65,7 +65,7 @@
/**
* Returns the callback which should be called when a stylus {@link MotionEvent} occurs within
- * the delegate view's bounds.
+ * the delegate view's bounds. The callback should only be called from the UI thread.
*/
@NonNull
public Runnable getInitiationCallback() {
diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
new file mode 100644
index 0000000..85f5056
--- /dev/null
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.accessibility;
+
+import android.accessibilityservice.AccessibilityGestureEvent;
+import android.accessibilityservice.AccessibilityService;
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.MagnificationConfig;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.graphics.Region;
+import android.os.IBinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
+import com.android.internal.inputmethod.RemoteAccessibilityInputConnection;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a privileged app - an app with MANAGE_ACCESSIBILITY permission and SystemAPI access - to
+ * interact with the windows in the display that this proxy represents. Proxying the default display
+ * or a display that is not tracked will throw an exception. Only the real user has access to global
+ * clients like SystemUI.
+ *
+ * <p>
+ * To register and unregister a proxy, use
+ * {@link AccessibilityManager#registerDisplayProxy(AccessibilityDisplayProxy)}
+ * and {@link AccessibilityManager#unregisterDisplayProxy(AccessibilityDisplayProxy)}. If the app
+ * that has registered the proxy dies, the system will remove the proxy.
+ *
+ * TODO(241429275): Complete proxy impl and add additional support (if necessary) like cache methods
+ * @hide
+ */
+@SystemApi
+public abstract class AccessibilityDisplayProxy {
+ private static final String LOG_TAG = "AccessibilityDisplayProxy";
+ private static final int INVALID_CONNECTION_ID = -1;
+
+ private List<AccessibilityServiceInfo> mInstalledAndEnabledServices;
+ private Executor mExecutor;
+ private int mConnectionId = INVALID_CONNECTION_ID;
+ private int mDisplayId;
+ IAccessibilityServiceClient mServiceClient;
+
+ /**
+ * Constructs an AccessibilityDisplayProxy instance.
+ * @param displayId the id of the display to proxy.
+ * @param executor the executor used to execute proxy callbacks.
+ * @param installedAndEnabledServices the list of infos representing the installed and
+ * enabled a11y services.
+ */
+ public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor,
+ @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) {
+ mDisplayId = displayId;
+ mExecutor = executor;
+ // Typically, the context is the Service context of an accessibility service.
+ // Context is used for ResolveInfo check, which a proxy won't have, IME input
+ // (FLAG_INPUT_METHOD_EDITOR), which the proxy doesn't need, and tracing
+ // A11yInteractionClient methods.
+ // TODO(254097475): Enable tracing, potentially without exposing Context.
+ mServiceClient = new IAccessibilityServiceClientImpl(null, mExecutor);
+ mInstalledAndEnabledServices = installedAndEnabledServices;
+ }
+
+ /**
+ * Returns the id of the display being proxy-ed.
+ */
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ /**
+ * An IAccessibilityServiceClient that handles interrupts and accessibility events.
+ */
+ private class IAccessibilityServiceClientImpl extends
+ AccessibilityService.IAccessibilityServiceClientWrapper {
+
+ IAccessibilityServiceClientImpl(Context context, Executor executor) {
+ super(context, executor, new AccessibilityService.Callbacks() {
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ // TODO: call AccessiiblityProxy.onAccessibilityEvent
+ }
+
+ @Override
+ public void onInterrupt() {
+ // TODO: call AccessiiblityProxy.onInterrupt
+ }
+ @Override
+ public void onServiceConnected() {
+ // TODO: send service infos and call AccessiiblityProxy.onProxyConnected
+ }
+ @Override
+ public void init(int connectionId, IBinder windowToken) {
+ mConnectionId = connectionId;
+ }
+
+ @Override
+ public boolean onGesture(AccessibilityGestureEvent gestureInfo) {
+ return false;
+ }
+
+ @Override
+ public boolean onKeyEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public void onMagnificationChanged(int displayId, @NonNull Region region,
+ MagnificationConfig config) {
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent event) {
+ }
+
+ @Override
+ public void onTouchStateChanged(int displayId, int state) {
+ }
+
+ @Override
+ public void onSoftKeyboardShowModeChanged(int showMode) {
+ }
+
+ @Override
+ public void onPerformGestureResult(int sequence, boolean completedSuccessfully) {
+ }
+
+ @Override
+ public void onFingerprintCapturingGesturesChanged(boolean active) {
+ }
+
+ @Override
+ public void onFingerprintGesture(int gesture) {
+ }
+
+ @Override
+ public void onAccessibilityButtonClicked(int displayId) {
+ }
+
+ @Override
+ public void onAccessibilityButtonAvailabilityChanged(boolean available) {
+ }
+
+ @Override
+ public void onSystemActionsChanged() {
+ }
+
+ @Override
+ public void createImeSession(IAccessibilityInputMethodSessionCallback callback) {
+ }
+
+ @Override
+ public void startInput(@Nullable RemoteAccessibilityInputConnection inputConnection,
+ @NonNull EditorInfo editorInfo, boolean restarting) {
+ }
+ });
+ }
+ }
+}
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 5433fa0..423c560 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -1921,6 +1921,67 @@
}
}
+ /**
+ * Registers an {@link AccessibilityDisplayProxy}, so this proxy can access UI content specific
+ * to its display.
+ *
+ * @param proxy the {@link AccessibilityDisplayProxy} to register.
+ * @return {@code true} if the proxy is successfully registered.
+ *
+ * @throws IllegalArgumentException if the proxy's display is not currently tracked by a11y, is
+ * {@link android.view.Display#DEFAULT_DISPLAY}, is or lower than
+ * {@link android.view.Display#INVALID_DISPLAY}, or is already being proxy-ed.
+ *
+ * @throws SecurityException if the app does not hold the
+ * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+ public boolean registerDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return false;
+ }
+ }
+
+ try {
+ return service.registerProxyForDisplay(proxy.mServiceClient, proxy.getDisplayId());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregisters an {@link AccessibilityDisplayProxy}.
+ *
+ * @return {@code true} if the proxy is successfully unregistered.
+ *
+ * @throws SecurityException if the app does not hold the
+ * {@link Manifest.permission#MANAGE_ACCESSIBILITY} permission.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+ public boolean unregisterDisplayProxy(@NonNull AccessibilityDisplayProxy proxy) {
+ final IAccessibilityManager service;
+ synchronized (mLock) {
+ service = getServiceLocked();
+ if (service == null) {
+ return false;
+ }
+ }
+ try {
+ return service.unregisterProxyForDisplay(proxy.getDisplayId());
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
private IAccessibilityManager getServiceLocked() {
if (mService == null) {
tryConnectToServiceLocked(null);
diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl
index 36fdcce4..a251948 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -109,9 +109,9 @@
oneway void setAccessibilityWindowAttributes(int displayId, int windowId, int userId, in AccessibilityWindowAttributes attributes);
- // Requires Manifest.permission.MANAGE_ACCESSIBILITY
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
boolean registerProxyForDisplay(IAccessibilityServiceClient proxy, int displayId);
- // Requires Manifest.permission.MANAGE_ACCESSIBILITY
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
boolean unregisterProxyForDisplay(int displayId);
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 70cfc3e..ef683b7 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -19,6 +19,7 @@
import static android.service.autofill.FillRequest.FLAG_IME_SHOWING;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE;
+import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE;
import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG;
import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED;
import static android.view.ContentInfo.SOURCE_AUTOFILL;
@@ -734,7 +735,7 @@
* Autofill will automatically trigger a fill request after activity
* start if there is any field is autofillable. But if there is a field that
* triggered autofill, it is unnecessary to trigger again through
- * AutofillManager#notifyViewEnteredForActivityStarted.
+ * AutofillManager#notifyViewEnteredForFillDialog.
*/
private AtomicBoolean mIsFillRequested;
@@ -747,6 +748,10 @@
private final String[] mFillDialogEnabledHints;
+ // Tracked all views that have appeared, including views that there are no
+ // dataset in responses. Used to avoid request pre-fill request again and again.
+ private final ArraySet<AutofillId> mAllTrackedViews = new ArraySet<>();
+
/** @hide */
public interface AutofillClient {
/**
@@ -1192,6 +1197,16 @@
* @hide
*/
public void notifyViewEnteredForFillDialog(View v) {
+ synchronized (mLock) {
+ if (mTrackedViews != null) {
+ // To support the fill dialog can show for the autofillable Views in
+ // different pages but in the same Activity. We need to reset the
+ // mIsFillRequested flag to allow asking for a new FillRequest when
+ // user switches to other page
+ mTrackedViews.checkViewState(v.getAutofillId());
+ }
+ }
+
// Skip if the fill request has been performed for a view.
if (mIsFillRequested.get()) {
return;
@@ -1318,6 +1333,10 @@
}
mForAugmentedAutofillOnly = false;
}
+
+ if ((flags & FLAG_SUPPORTS_FILL_DIALOG) != 0) {
+ flags |= FLAG_RESET_FILL_DIALOG_STATE;
+ }
updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags);
}
addEnteredIdLocked(id);
@@ -2217,6 +2236,7 @@
mIsFillRequested.set(false);
mShowAutofillDialogCalled = false;
mFillDialogTriggerIds = null;
+ mAllTrackedViews.clear();
if (resetEnteredIds) {
mEnteredIds = null;
}
@@ -2776,14 +2796,9 @@
+ ", mFillableIds=" + mFillableIds
+ ", mEnabled=" + mEnabled
+ ", mSessionId=" + mSessionId);
-
}
+
if (mEnabled && mSessionId == sessionId) {
- if (saveOnAllViewsInvisible) {
- mTrackedViews = new TrackedViews(trackedIds);
- } else {
- mTrackedViews = null;
- }
mSaveOnFinish = saveOnFinish;
if (fillableIds != null) {
if (mFillableIds == null) {
@@ -2805,6 +2820,27 @@
mSaveTriggerId = saveTriggerId;
setNotifyOnClickLocked(mSaveTriggerId, true);
}
+
+ if (!saveOnAllViewsInvisible) {
+ trackedIds = null;
+ }
+
+ final ArraySet<AutofillId> allFillableIds = new ArraySet<>();
+ if (mFillableIds != null) {
+ allFillableIds.addAll(mFillableIds);
+ }
+ if (trackedIds != null) {
+ for (AutofillId id : trackedIds) {
+ id.resetSessionId();
+ allFillableIds.add(id);
+ }
+ }
+
+ if (!allFillableIds.isEmpty()) {
+ mTrackedViews = new TrackedViews(trackedIds, Helper.toArray(allFillableIds));
+ } else {
+ mTrackedViews = null;
+ }
}
}
}
@@ -3576,10 +3612,19 @@
*/
private class TrackedViews {
/** Visible tracked views */
- @Nullable private ArraySet<AutofillId> mVisibleTrackedIds;
+ @NonNull private final ArraySet<AutofillId> mVisibleTrackedIds;
/** Invisible tracked views */
- @Nullable private ArraySet<AutofillId> mInvisibleTrackedIds;
+ @NonNull private final ArraySet<AutofillId> mInvisibleTrackedIds;
+
+ /** Visible tracked views for fill dialog */
+ @NonNull private final ArraySet<AutofillId> mVisibleDialogTrackedIds;
+
+ /** Invisible tracked views for fill dialog */
+ @NonNull private final ArraySet<AutofillId> mInvisibleDialogTrackedIds;
+
+ boolean mHasNewTrackedView;
+ boolean mIsTrackedSaveView;
/**
* Check if set is null or value is in set.
@@ -3645,43 +3690,65 @@
*
* @param trackedIds The views to be tracked
*/
- TrackedViews(@Nullable AutofillId[] trackedIds) {
- final AutofillClient client = getClient();
- if (!ArrayUtils.isEmpty(trackedIds) && client != null) {
- final boolean[] isVisible;
+ TrackedViews(@Nullable AutofillId[] trackedIds, @Nullable AutofillId[] allTrackedIds) {
+ mVisibleTrackedIds = new ArraySet<>();
+ mInvisibleTrackedIds = new ArraySet<>();
+ if (!ArrayUtils.isEmpty(trackedIds)) {
+ mIsTrackedSaveView = true;
+ initialTrackedViews(trackedIds, mVisibleTrackedIds, mInvisibleTrackedIds);
+ }
- if (client.autofillClientIsVisibleForAutofill()) {
- if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
- isVisible = client.autofillClientGetViewVisibility(trackedIds);
- } else {
- // All false
- isVisible = new boolean[trackedIds.length];
- }
-
- final int numIds = trackedIds.length;
- for (int i = 0; i < numIds; i++) {
- final AutofillId id = trackedIds[i];
- id.resetSessionId();
-
- if (isVisible[i]) {
- mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
- } else {
- mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
- }
- }
+ mVisibleDialogTrackedIds = new ArraySet<>();
+ mInvisibleDialogTrackedIds = new ArraySet<>();
+ if (!ArrayUtils.isEmpty(allTrackedIds)) {
+ initialTrackedViews(allTrackedIds, mVisibleDialogTrackedIds,
+ mInvisibleDialogTrackedIds);
+ mAllTrackedViews.addAll(Arrays.asList(allTrackedIds));
}
if (sVerbose) {
Log.v(TAG, "TrackedViews(trackedIds=" + Arrays.toString(trackedIds) + "): "
+ " mVisibleTrackedIds=" + mVisibleTrackedIds
- + " mInvisibleTrackedIds=" + mInvisibleTrackedIds);
+ + " mInvisibleTrackedIds=" + mInvisibleTrackedIds
+ + " allTrackedIds=" + Arrays.toString(allTrackedIds)
+ + " mVisibleDialogTrackedIds=" + mVisibleDialogTrackedIds
+ + " mInvisibleDialogTrackedIds=" + mInvisibleDialogTrackedIds);
}
- if (mVisibleTrackedIds == null) {
+ if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
}
}
+ private void initialTrackedViews(AutofillId[] trackedIds,
+ @NonNull ArraySet<AutofillId> visibleSet,
+ @NonNull ArraySet<AutofillId> invisibleSet) {
+ final boolean[] isVisible;
+ final AutofillClient client = getClient();
+ if (ArrayUtils.isEmpty(trackedIds) || client == null) {
+ return;
+ }
+ if (client.autofillClientIsVisibleForAutofill()) {
+ if (sVerbose) Log.v(TAG, "client is visible, check tracked ids");
+ isVisible = client.autofillClientGetViewVisibility(trackedIds);
+ } else {
+ // All false
+ isVisible = new boolean[trackedIds.length];
+ }
+
+ final int numIds = trackedIds.length;
+ for (int i = 0; i < numIds; i++) {
+ final AutofillId id = trackedIds[i];
+ id.resetSessionId();
+
+ if (isVisible[i]) {
+ addToSet(visibleSet, id);
+ } else {
+ addToSet(invisibleSet, id);
+ }
+ }
+ }
+
/**
* Called when a {@link View view's} visibility changes.
*
@@ -3698,22 +3765,37 @@
if (isClientVisibleForAutofillLocked()) {
if (isVisible) {
if (isInSet(mInvisibleTrackedIds, id)) {
- mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
- mVisibleTrackedIds = addToSet(mVisibleTrackedIds, id);
+ removeFromSet(mInvisibleTrackedIds, id);
+ addToSet(mVisibleTrackedIds, id);
+ }
+ if (isInSet(mInvisibleDialogTrackedIds, id)) {
+ removeFromSet(mInvisibleDialogTrackedIds, id);
+ addToSet(mVisibleDialogTrackedIds, id);
}
} else {
if (isInSet(mVisibleTrackedIds, id)) {
- mVisibleTrackedIds = removeFromSet(mVisibleTrackedIds, id);
- mInvisibleTrackedIds = addToSet(mInvisibleTrackedIds, id);
+ removeFromSet(mVisibleTrackedIds, id);
+ addToSet(mInvisibleTrackedIds, id);
+ }
+ if (isInSet(mVisibleDialogTrackedIds, id)) {
+ removeFromSet(mVisibleDialogTrackedIds, id);
+ addToSet(mInvisibleDialogTrackedIds, id);
}
}
}
- if (mVisibleTrackedIds == null) {
+ if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
if (sVerbose) {
Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleTrackedIds);
}
finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
+
+ }
+ if (mVisibleDialogTrackedIds.isEmpty()) {
+ if (sVerbose) {
+ Log.v(TAG, "No more visible ids. Invisible = " + mInvisibleDialogTrackedIds);
+ }
+ processNoVisibleTrackedAllViews();
}
}
@@ -3727,66 +3809,66 @@
// The visibility of the views might have changed while the client was not be visible,
// hence update the visibility state for all views.
AutofillClient client = getClient();
- ArraySet<AutofillId> updatedVisibleTrackedIds = null;
- ArraySet<AutofillId> updatedInvisibleTrackedIds = null;
if (client != null) {
if (sVerbose) {
Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + mInvisibleTrackedIds
+ " vis=" + mVisibleTrackedIds);
}
- if (mInvisibleTrackedIds != null) {
- final ArrayList<AutofillId> orderedInvisibleIds =
- new ArrayList<>(mInvisibleTrackedIds);
- final boolean[] isVisible = client.autofillClientGetViewVisibility(
- Helper.toArray(orderedInvisibleIds));
- final int numInvisibleTrackedIds = orderedInvisibleIds.size();
- for (int i = 0; i < numInvisibleTrackedIds; i++) {
- final AutofillId id = orderedInvisibleIds.get(i);
- if (isVisible[i]) {
- updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
-
- if (sDebug) {
- Log.d(TAG, "onVisibleForAutofill() " + id + " became visible");
- }
- } else {
- updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
- }
- }
- }
-
- if (mVisibleTrackedIds != null) {
- final ArrayList<AutofillId> orderedVisibleIds =
- new ArrayList<>(mVisibleTrackedIds);
- final boolean[] isVisible = client.autofillClientGetViewVisibility(
- Helper.toArray(orderedVisibleIds));
-
- final int numVisibleTrackedIds = orderedVisibleIds.size();
- for (int i = 0; i < numVisibleTrackedIds; i++) {
- final AutofillId id = orderedVisibleIds.get(i);
-
- if (isVisible[i]) {
- updatedVisibleTrackedIds = addToSet(updatedVisibleTrackedIds, id);
- } else {
- updatedInvisibleTrackedIds = addToSet(updatedInvisibleTrackedIds, id);
-
- if (sDebug) {
- Log.d(TAG, "onVisibleForAutofill() " + id + " became invisible");
- }
- }
- }
- }
-
- mInvisibleTrackedIds = updatedInvisibleTrackedIds;
- mVisibleTrackedIds = updatedVisibleTrackedIds;
+ onVisibleForAutofillChangedInternalLocked(mVisibleTrackedIds, mInvisibleTrackedIds);
+ onVisibleForAutofillChangedInternalLocked(
+ mVisibleDialogTrackedIds, mInvisibleDialogTrackedIds);
}
- if (mVisibleTrackedIds == null) {
+ if (mIsTrackedSaveView && mVisibleTrackedIds.isEmpty()) {
if (sVerbose) {
- Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
+ Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
}
finishSessionLocked(/* commitReason= */ COMMIT_REASON_VIEW_CHANGED);
}
+ if (mVisibleDialogTrackedIds.isEmpty()) {
+ if (sVerbose) {
+ Log.v(TAG, "onVisibleForAutofillChangedLocked(): no more visible ids");
+ }
+ processNoVisibleTrackedAllViews();
+ }
+ }
+
+ void onVisibleForAutofillChangedInternalLocked(@NonNull ArraySet<AutofillId> visibleSet,
+ @NonNull ArraySet<AutofillId> invisibleSet) {
+ // The visibility of the views might have changed while the client was not be visible,
+ // hence update the visibility state for all views.
+ if (sVerbose) {
+ Log.v(TAG, "onVisibleForAutofillChangedLocked(): inv= " + invisibleSet
+ + " vis=" + visibleSet);
+ }
+
+ ArraySet<AutofillId> allTrackedIds = new ArraySet<>();
+ allTrackedIds.addAll(visibleSet);
+ allTrackedIds.addAll(invisibleSet);
+ if (!allTrackedIds.isEmpty()) {
+ visibleSet.clear();
+ invisibleSet.clear();
+ initialTrackedViews(Helper.toArray(allTrackedIds), visibleSet, invisibleSet);
+ }
+ }
+
+ private void processNoVisibleTrackedAllViews() {
+ mShowAutofillDialogCalled = false;
+ }
+
+ void checkViewState(AutofillId id) {
+ if (mAllTrackedViews.contains(id)) {
+ return;
+ }
+ // Add the id as tracked to avoid triggering fill request again and again.
+ mAllTrackedViews.add(id);
+ if (mHasNewTrackedView) {
+ return;
+ }
+ // First one new tracks view
+ mIsFillRequested.set(false);
+ mHasNewTrackedView = true;
}
}
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 1664637..d067d4b 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -378,7 +378,7 @@
private final Object mLock = new Object();
@NonNull
- private final Context mContext;
+ private final StrippedContext mContext;
@NonNull
private final IContentCaptureManager mService;
@@ -414,9 +414,37 @@
}
/** @hide */
+ static class StrippedContext {
+ final String mPackageName;
+ final String mContext;
+ final @UserIdInt int mUserId;
+
+ private StrippedContext(Context context) {
+ mPackageName = context.getPackageName();
+ mContext = context.toString();
+ mUserId = context.getUserId();
+ }
+
+ @Override
+ public String toString() {
+ return mContext;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @UserIdInt
+ public int getUserId() {
+ return mUserId;
+ }
+ }
+
+ /** @hide */
public ContentCaptureManager(@NonNull Context context,
@NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) {
- mContext = Objects.requireNonNull(context, "context cannot be null");
+ Objects.requireNonNull(context, "context cannot be null");
+ mContext = new StrippedContext(context);
mService = Objects.requireNonNull(service, "service cannot be null");
mOptions = Objects.requireNonNull(options, "options cannot be null");
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index c32ca9e..a989558 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -36,7 +36,6 @@
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.ComponentName;
-import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.graphics.Insets;
import android.graphics.Rect;
@@ -103,7 +102,7 @@
private final AtomicBoolean mDisabled = new AtomicBoolean(false);
@NonNull
- private final Context mContext;
+ private final ContentCaptureManager.StrippedContext mContext;
@NonNull
private final ContentCaptureManager mManager;
@@ -197,7 +196,7 @@
}
}
- protected MainContentCaptureSession(@NonNull Context context,
+ protected MainContentCaptureSession(@NonNull ContentCaptureManager.StrippedContext context,
@NonNull ContentCaptureManager manager, @NonNull Handler handler,
@NonNull IContentCaptureManager systemServerInterface) {
mContext = context;
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index 1afa987..a66c67b 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -107,8 +107,8 @@
* @param where where the information is coming from.
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresNoPermission
@AnyThread
+ @RequiresNoPermission
static void startProtoDump(byte[] protoDump, int source, String where,
@Nullable Consumer<RemoteException> exceptionHandler) {
final IInputMethodManager service = getService();
@@ -127,8 +127,8 @@
*
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
@AnyThread
+ @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -146,8 +146,8 @@
*
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
@AnyThread
+ @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -165,8 +165,8 @@
*
* @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}.
*/
- @RequiresNoPermission
@AnyThread
+ @RequiresNoPermission
static boolean isImeTraceEnabled() {
final IInputMethodManager service = getService();
if (service == null) {
@@ -182,8 +182,8 @@
/**
* Invokes {@link IInputMethodManager#removeImeSurface()}
*/
- @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@AnyThread
+ @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -212,6 +212,7 @@
@AnyThread
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static List<InputMethodInfo> getInputMethodList(@UserIdInt int userId,
@DirectBootAwareness int directBootAwareness) {
final IInputMethodManager service = getService();
@@ -227,6 +228,7 @@
@AnyThread
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -241,6 +243,7 @@
@AnyThread
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(@Nullable String imiId,
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) {
final IInputMethodManager service = getService();
@@ -257,6 +260,7 @@
@AnyThread
@Nullable
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -302,6 +306,7 @@
@AnyThread
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static InputBindResult startInputOrWindowGainedFocus(@StartInputReason int startInputReason,
@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
@StartInputFlags int startInputFlags,
@@ -340,14 +345,14 @@
}
@AnyThread
- static void showInputMethodPickerFromSystem(@NonNull IInputMethodClient client,
- int auxiliarySubtypeMode, int displayId) {
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ static void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
final IInputMethodManager service = getService();
if (service == null) {
return;
}
try {
- service.showInputMethodPickerFromSystem(client, auxiliarySubtypeMode, displayId);
+ service.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -369,6 +374,7 @@
@AnyThread
@Nullable
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -382,6 +388,7 @@
}
@AnyThread
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static void setAdditionalInputMethodSubtypes(@NonNull String imeId,
@NonNull InputMethodSubtype[] subtypes, @UserIdInt int userId) {
final IInputMethodManager service = getService();
@@ -396,6 +403,7 @@
}
@AnyThread
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static void setExplicitlyEnabledInputMethodSubtypes(@NonNull String imeId,
@NonNull int[] subtypeHashCodes, @UserIdInt int userId) {
final IInputMethodManager service = getService();
@@ -476,6 +484,7 @@
}
@AnyThread
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -503,6 +512,7 @@
}
@AnyThread
+ @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
static void setStylusWindowIdleTimeoutForTest(
IInputMethodClient client, @DurationMillisLong long timeout) {
final IInputMethodManager service = getService();
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 9106ce2..3278242 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -16,8 +16,6 @@
package android.view.inputmethod;
-import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
-import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE;
import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR;
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID;
@@ -1525,12 +1523,18 @@
/**
* Returns {@code true} if currently selected IME supports Stylus handwriting & is enabled for
* the given userId.
- * If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be
- * called and Stylus touch should continue as normal touch input.
+ *
+ * <p>If the method returns {@code false}, {@link #startStylusHandwriting(View)} shouldn't be
+ * called and Stylus touch should continue as normal touch input.</p>
+ *
+ * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+ * {@code userId} is different from the user id of the current process.</p>
+ *
* @see #startStylusHandwriting(View)
* @param userId user ID to query.
* @hide
*/
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId) {
final Context fallbackContext = ActivityThread.currentApplication();
if (fallbackContext == null) {
@@ -1549,13 +1553,16 @@
/**
* Returns the list of installed input methods for the specified user.
*
+ * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+ * {@code userId} is different from the user id of the current process.</p>
+ *
* @param userId user ID to query
* @return {@link List} of {@link InputMethodInfo}.
* @hide
*/
@TestApi
- @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId) {
return IInputMethodManagerGlobalInvoker.getInputMethodList(userId,
DirectBootAwareness.AUTO);
@@ -1564,14 +1571,17 @@
/**
* Returns the list of installed input methods for the specified user.
*
+ * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+ * {@code userId} is different from the user id of the current process.</p>
+ *
* @param userId user ID to query
* @param directBootAwareness {@code true} if caller want to query installed input methods list
* on user locked state.
* @return {@link List} of {@link InputMethodInfo}.
* @hide
*/
- @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
@NonNull
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
public List<InputMethodInfo> getInputMethodListAsUser(@UserIdInt int userId,
@DirectBootAwareness int directBootAwareness) {
return IInputMethodManagerGlobalInvoker.getInputMethodList(userId, directBootAwareness);
@@ -1595,11 +1605,14 @@
/**
* Returns the list of enabled input methods for the specified user.
*
+ * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+ * {@code userId} is different from the user id of the current process.</p>
+ *
* @param userId user ID to query
* @return {@link List} of {@link InputMethodInfo}.
* @hide
*/
- @RequiresPermission(INTERACT_ACROSS_USERS_FULL)
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
public List<InputMethodInfo> getEnabledInputMethodListAsUser(@UserIdInt int userId) {
return IInputMethodManagerGlobalInvoker.getEnabledInputMethodList(userId);
}
@@ -2352,7 +2365,11 @@
* Starts an input connection from the served view that gains the window focus.
* Note that this method should *NOT* be called inside of {@code mH} lock to prevent start input
* background thread may blocked by other methods which already inside {@code mH} lock.
+ *
+ * <p>{@link Manifest.permission#INTERACT_ACROSS_USERS_FULL} is required when and only when
+ * {@code userId} is different from the user id of the current process.</p>
*/
+ @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
private boolean startInputInner(@StartInputReason int startInputReason,
@Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
@SoftInputModeFlags int softInputMode, int windowFlags) {
@@ -2609,8 +2626,8 @@
* @param timeout to set in milliseconds. To reset to default, use a value <= zero.
* @hide
*/
- @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
@TestApi
+ @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
public void setStylusWindowIdleTimeoutForTest(@DurationMillisLong long timeout) {
synchronized (mH) {
IInputMethodManagerGlobalInvoker.setStylusWindowIdleTimeoutForTest(mClient, timeout);
@@ -3089,7 +3106,7 @@
*
* <p>On Android {@link Build.VERSION_CODES#Q} and later devices, the undocumented behavior that
* token can be {@code null} when the caller has
- * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update
+ * {@link Manifest.permission#WRITE_SECURE_SETTINGS} is deprecated. Instead, update
* {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
* {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
*
@@ -3121,7 +3138,7 @@
if (fallbackContext == null) {
return;
}
- if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+ if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
@@ -3158,7 +3175,7 @@
* from an application or a service which has a token of the currently active input method.
*
* <p>On Android {@link Build.VERSION_CODES#Q} and later devices, {@code token} cannot be
- * {@code null} even with {@link android.Manifest.permission#WRITE_SECURE_SETTINGS}. Instead,
+ * {@code null} even with {@link Manifest.permission#WRITE_SECURE_SETTINGS}. Instead,
* update {@link android.provider.Settings.Secure#DEFAULT_INPUT_METHOD} and
* {@link android.provider.Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE} directly.</p>
*
@@ -3440,12 +3457,12 @@
* @param displayId The ID of the display where the chooser dialog should be shown.
* @hide
*/
- @RequiresPermission(WRITE_SECURE_SETTINGS)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public void showInputMethodPickerFromSystem(boolean showAuxiliarySubtypes, int displayId) {
final int mode = showAuxiliarySubtypes
? SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES
: SHOW_IM_PICKER_MODE_EXCLUDE_AUXILIARY_SUBTYPES;
- IInputMethodManagerGlobalInvoker.showInputMethodPickerFromSystem(mClient, mode, displayId);
+ IInputMethodManagerGlobalInvoker.showInputMethodPickerFromSystem(mode, displayId);
}
@GuardedBy("mH")
@@ -3519,11 +3536,11 @@
* {@link InputMethodService#switchInputMethod(String, InputMethodSubtype)}, which
* does not require any permission as long as the caller is the current IME.
* If the calling process is some privileged app that already has
- * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} permission, just
+ * {@link Manifest.permission#WRITE_SECURE_SETTINGS} permission, just
* directly update {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}.
*/
@Deprecated
- @RequiresPermission(WRITE_SECURE_SETTINGS)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) {
if (Process.myUid() == Process.SYSTEM_UID) {
Log.w(TAG, "System process should not call setCurrentInputMethodSubtype() because "
@@ -3540,7 +3557,7 @@
if (fallbackContext == null) {
return false;
}
- if (fallbackContext.checkSelfPermission(WRITE_SECURE_SETTINGS)
+ if (fallbackContext.checkSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
diff --git a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
index 63d9167..5df9fd1 100644
--- a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
+++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java
@@ -16,6 +16,7 @@
package android.view.inputmethod;
+import android.Manifest;
import android.annotation.AnyThread;
import android.annotation.Nullable;
import android.annotation.RequiresNoPermission;
@@ -51,8 +52,8 @@
* @param where where the information is coming from.
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresNoPermission
@AnyThread
+ @RequiresNoPermission
public static void startProtoDump(byte[] protoDump, int source, String where,
@Nullable Consumer<RemoteException> exceptionHandler) {
IInputMethodManagerGlobalInvoker.startProtoDump(protoDump, source, where, exceptionHandler);
@@ -63,8 +64,8 @@
*
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
@AnyThread
+ @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
public static void startImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
IInputMethodManagerGlobalInvoker.startImeTrace(exceptionHandler);
}
@@ -74,8 +75,8 @@
*
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresPermission(android.Manifest.permission.CONTROL_UI_TRACING)
@AnyThread
+ @RequiresPermission(Manifest.permission.CONTROL_UI_TRACING)
public static void stopImeTrace(@Nullable Consumer<RemoteException> exceptionHandler) {
IInputMethodManagerGlobalInvoker.stopImeTrace(exceptionHandler);
}
@@ -85,8 +86,8 @@
*
* @return The return value of {@link IInputMethodManager#isImeTraceEnabled()}.
*/
- @RequiresNoPermission
@AnyThread
+ @RequiresNoPermission
public static boolean isImeTraceEnabled() {
return IInputMethodManagerGlobalInvoker.isImeTraceEnabled();
}
@@ -96,8 +97,8 @@
*
* @param exceptionHandler an optional {@link RemoteException} handler.
*/
- @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)
@AnyThread
+ @RequiresPermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW)
public static void removeImeSurface(@Nullable Consumer<RemoteException> exceptionHandler) {
IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler);
}
diff --git a/core/java/android/view/inputmethod/InsertGesture.java b/core/java/android/view/inputmethod/InsertGesture.java
index 9f03289..0449a16 100644
--- a/core/java/android/view/inputmethod/InsertGesture.java
+++ b/core/java/android/view/inputmethod/InsertGesture.java
@@ -21,7 +21,6 @@
import android.graphics.PointF;
import android.os.Parcel;
import android.os.Parcelable;
-import android.text.TextUtils;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -52,7 +51,8 @@
mPoint = source.readTypedObject(PointF.CREATOR);
}
- /** Returns the text that will be inserted at {@link #getInsertionPoint()} **/
+ /** Returns the text that will be inserted at {@link #getInsertionPoint()}. When text is
+ * empty, cursor should be moved the insertion point. **/
@NonNull
public String getTextToInsert() {
return mTextToInsert;
@@ -75,7 +75,11 @@
private PointF mPoint;
private String mFallbackText;
- /** set the text that will be inserted at {@link #setInsertionPoint(PointF)} **/
+ /**
+ * Set the text that will be inserted at {@link #setInsertionPoint(PointF)}. When set with
+ * an empty string, cursor will be moved to {@link #getInsertionPoint()} and no text
+ * would be inserted.
+ */
@NonNull
@SuppressLint("MissingGetterMatchingBuilder")
public Builder setTextToInsert(@NonNull String text) {
@@ -114,8 +118,8 @@
if (mPoint == null) {
throw new IllegalArgumentException("Insertion point must be set.");
}
- if (TextUtils.isEmpty(mText)) {
- throw new IllegalArgumentException("Text to insert must be non-empty.");
+ if (mText == null) {
+ throw new IllegalArgumentException("Text to insert must be set.");
}
return new InsertGesture(mText, mPoint, mFallbackText);
}
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index f7bb16e..40c6a05 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -81,8 +81,7 @@
@EnforcePermission("WRITE_SECURE_SETTINGS")
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
- void showInputMethodPickerFromSystem(in IInputMethodClient client,
- int auxiliarySubtypeMode, int displayId);
+ void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId);
@EnforcePermission("TEST_INPUT_METHOD")
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d8ecb5c..16e0a59 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1150,6 +1150,18 @@
android:description="@string/permdesc_readMediaImages"
android:protectionLevel="dangerous" />
+ <!-- Allows an application to read image or video files from external storage that a user has
+ selected via the permission prompt photo picker. Apps can check this permission to verify that
+ a user has decided to use the photo picker, instead of granting access to
+ {@link #READ_MEDIA_IMAGES or #READ_MEDIA_VIDEO}. It does not prevent apps from accessing the
+ standard photo picker manually.
+ <p>Protection level: dangerous -->
+ <permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
+ android:permissionGroup="android.permission-group.UNDEFINED"
+ android:label="@string/permlab_readVisualUserSelect"
+ android:description="@string/permdesc_readVisualUserSelect"
+ android:protectionLevel="dangerous" />
+
<!-- Allows an application to write to external storage.
<p><strong>Note: </strong>If your app targets {@link android.os.Build.VERSION_CODES#R} or
higher, this permission has no effect.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 23dd1b4..a1f46fc 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4425,13 +4425,13 @@
<string name="config_mediaProjectionPermissionDialogComponent" translatable="false">com.android.systemui/com.android.systemui.media.MediaProjectionPermissionActivity</string>
<!-- Corner radius of system dialogs -->
- <dimen name="config_dialogCornerRadius">2dp</dimen>
+ <dimen name="config_dialogCornerRadius">28dp</dimen>
<!-- Corner radius of system buttons -->
- <dimen name="config_buttonCornerRadius">@dimen/control_corner_material</dimen>
+ <dimen name="config_buttonCornerRadius">4dp</dimen>
<!-- Corner radius for bottom sheet system dialogs -->
- <dimen name="config_bottomDialogCornerRadius">@dimen/config_dialogCornerRadius</dimen>
+ <dimen name="config_bottomDialogCornerRadius">16dp</dimen>
<!-- Corner radius of system progress bars -->
- <dimen name="config_progressBarCornerRadius">@dimen/progress_bar_corner_material</dimen>
+ <dimen name="config_progressBarCornerRadius">1000dp</dimen>
<!-- Controls whether system buttons use all caps for text -->
<bool name="config_buttonTextAllCaps">true</bool>
<!-- Name of the font family used for system surfaces where the font should use medium weight -->
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 1997261..9dbb6a0 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -979,9 +979,9 @@
<dimen name="controls_thumbnail_image_max_width">280dp</dimen>
<!-- System-provided radius for the background view of app widgets. The resolved value of this resource may change at runtime. -->
- <dimen name="system_app_widget_background_radius">16dp</dimen>
+ <dimen name="system_app_widget_background_radius">28dp</dimen>
<!-- System-provided radius for inner views on app widgets. The resolved value of this resource may change at runtime. -->
- <dimen name="system_app_widget_inner_radius">8dp</dimen>
+ <dimen name="system_app_widget_inner_radius">20dp</dimen>
<!-- System-provided padding for inner views on app widgets. The resolved value of this resource may change at runtime. @removed -->
<dimen name="__removed_system_app_widget_internal_padding">16dp</dimen>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 509de33..1f459c6 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1933,6 +1933,11 @@
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
<string name="permdesc_readMediaImages">Allows the app to read image files from your shared storage.</string>
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
+ <string name="permlab_readVisualUserSelect">read user selected image and video files from shared storage</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can read from. [CHAR LIMIT=none] -->
+ <string name="permdesc_readVisualUserSelect">Allows the app to read image and video files that you select from your shared storage.</string>
+
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] -->
<string name="permlab_sdcardWrite">modify or delete the contents of your shared storage</string>
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. "shared storage" refers to a storage space on the device that all apps with this permission can write to. [CHAR LIMIT=none] -->
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
index 57b9cb1..5bd018b 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java
@@ -23,11 +23,13 @@
import android.annotation.Nullable;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
+import android.os.Parcel;
import org.junit.Test;
public final class ProgramSelectorTest {
+ private static final int CREATOR_ARRAY_SIZE = 2;
private static final int FM_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_FM;
private static final int DAB_PROGRAM_TYPE = ProgramSelector.PROGRAM_TYPE_DAB;
private static final long FM_FREQUENCY = 88500;
@@ -97,6 +99,33 @@
}
@Test
+ public void describeContents_forIdentifier() {
+ assertWithMessage("FM identifier contents")
+ .that(FM_IDENTIFIER.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forIdentifierCreator() {
+ ProgramSelector.Identifier[] identifiers =
+ ProgramSelector.Identifier.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Identifiers").that(identifiers).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void writeToParcel_forIdentifier() {
+ Parcel parcel = Parcel.obtain();
+
+ FM_IDENTIFIER.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ ProgramSelector.Identifier identifierFromParcel =
+ ProgramSelector.Identifier.CREATOR.createFromParcel(parcel);
+ assertWithMessage("Identifier created from parcel")
+ .that(identifierFromParcel).isEqualTo(FM_IDENTIFIER);
+ }
+
+ @Test
public void getProgramType() {
ProgramSelector selector = getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
@@ -394,6 +423,34 @@
.that(selector1.strictEquals(selector2)).isTrue();
}
+ @Test
+ public void describeContents_forProgramSelector() {
+ assertWithMessage("FM selector contents")
+ .that(getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null)
+ .describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forProgramSelectorCreator() {
+ ProgramSelector[] programSelectors = ProgramSelector.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Program selectors").that(programSelectors).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void writeToParcel_forProgramSelector() {
+ ProgramSelector selectorExpected =
+ getFmSelector(/* secondaryIds= */ null, /* vendorIds= */ null);
+ Parcel parcel = Parcel.obtain();
+
+ selectorExpected.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ ProgramSelector selectorFromParcel = ProgramSelector.CREATOR.createFromParcel(parcel);
+ assertWithMessage("Program selector created from parcel")
+ .that(selectorFromParcel).isEqualTo(selectorExpected);
+ }
+
private ProgramSelector getFmSelector(@Nullable ProgramSelector.Identifier[] secondaryIds,
@Nullable long[] vendorIds) {
return new ProgramSelector(FM_PROGRAM_TYPE, FM_IDENTIFIER, secondaryIds, vendorIds);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
index 42143b9..6e1bb4b4 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioAnnouncementTest.java
@@ -22,6 +22,7 @@
import android.hardware.radio.Announcement;
import android.hardware.radio.ProgramSelector;
+import android.os.Parcel;
import android.util.ArrayMap;
import org.junit.Test;
@@ -83,4 +84,35 @@
vendorInfo.put("vendorKeyMock", "vendorValueMock");
return vendorInfo;
}
+
+ @Test
+ public void describeContents_forAnnouncement() {
+ assertWithMessage("Radio announcement contents")
+ .that(TEST_ANNOUNCEMENT.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forAnnouncementCreator() {
+ int sizeExpected = 2;
+
+ Announcement[] announcements = Announcement.CREATOR.newArray(sizeExpected);
+
+ assertWithMessage("Announcements").that(announcements).hasLength(sizeExpected);
+ }
+
+ @Test
+ public void writeToParcel_forAnnouncement() {
+ Parcel parcel = Parcel.obtain();
+
+ TEST_ANNOUNCEMENT.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ Announcement announcementFromParcel = Announcement.CREATOR.createFromParcel(parcel);
+ assertWithMessage("Selector of announcement created from parcel")
+ .that(announcementFromParcel.getSelector()).isEqualTo(FM_PROGRAM_SELECTOR);
+ assertWithMessage("Type of announcement created from parcel")
+ .that(announcementFromParcel.getType()).isEqualTo(TRAFFIC_ANNOUNCEMENT_TYPE);
+ assertWithMessage("Vendor info of announcement created from parcel")
+ .that(announcementFromParcel.getVendorInfo()).isEqualTo(VENDOR_INFO);
+ }
}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
index be4d0d4..f838a5d 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -33,6 +33,7 @@
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
+import android.os.Parcel;
import android.os.RemoteException;
import android.util.ArrayMap;
@@ -80,6 +81,8 @@
private static final int[] SUPPORTED_IDENTIFIERS_TYPES = new int[]{
ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI};
+ private static final int CREATOR_ARRAY_SIZE = 3;
+
private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR =
createFmBandDescriptor();
private static final RadioManager.AmBandDescriptor AM_BAND_DESCRIPTOR =
@@ -173,6 +176,22 @@
}
@Test
+ public void describeContents_forBandDescriptor() {
+ RadioManager.BandDescriptor bandDescriptor = createFmBandDescriptor();
+
+ assertWithMessage("Band Descriptor contents")
+ .that(bandDescriptor.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forBandDescriptorCreator() {
+ RadioManager.BandDescriptor[] bandDescriptors =
+ RadioManager.BandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Band Descriptors").that(bandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void isAmBand_forAmBandDescriptor_returnsTrue() {
RadioManager.BandDescriptor bandDescriptor = createAmBandDescriptor();
@@ -219,18 +238,73 @@
}
@Test
+ public void describeContents_forFmBandDescriptor() {
+ assertWithMessage("FM Band Descriptor contents")
+ .that(FM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel_forFmBandDescriptor() {
+ Parcel parcel = Parcel.obtain();
+
+ FM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioManager.FmBandDescriptor fmBandDescriptorFromParcel =
+ RadioManager.FmBandDescriptor.CREATOR.createFromParcel(parcel);
+ assertWithMessage("FM Band Descriptor created from parcel")
+ .that(fmBandDescriptorFromParcel).isEqualTo(FM_BAND_DESCRIPTOR);
+ }
+
+ @Test
+ public void newArray_forFmBandDescriptorCreator() {
+ RadioManager.FmBandDescriptor[] fmBandDescriptors =
+ RadioManager.FmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("FM Band Descriptors")
+ .that(fmBandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void isStereoSupported_forAmBandDescriptor() {
assertWithMessage("AM Band Descriptor stereo")
.that(AM_BAND_DESCRIPTOR.isStereoSupported()).isEqualTo(STEREO_SUPPORTED);
}
@Test
+ public void describeContents_forAmBandDescriptor() {
+ assertWithMessage("AM Band Descriptor contents")
+ .that(AM_BAND_DESCRIPTOR.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel_forAmBandDescriptor() {
+ Parcel parcel = Parcel.obtain();
+
+ AM_BAND_DESCRIPTOR.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioManager.AmBandDescriptor amBandDescriptorFromParcel =
+ RadioManager.AmBandDescriptor.CREATOR.createFromParcel(parcel);
+ assertWithMessage("FM Band Descriptor created from parcel")
+ .that(amBandDescriptorFromParcel).isEqualTo(AM_BAND_DESCRIPTOR);
+ }
+
+ @Test
+ public void newArray_forAmBandDescriptorCreator() {
+ RadioManager.AmBandDescriptor[] amBandDescriptors =
+ RadioManager.AmBandDescriptor.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("AM Band Descriptors")
+ .that(amBandDescriptors).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void equals_withSameFmBandDescriptors_returnsTrue() {
- RadioManager.FmBandDescriptor fmBandDescriptor1 = createFmBandDescriptor();
- RadioManager.FmBandDescriptor fmBandDescriptor2 = createFmBandDescriptor();
+ RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor();
assertWithMessage("The same FM Band Descriptor")
- .that(fmBandDescriptor1).isEqualTo(fmBandDescriptor2);
+ .that(FM_BAND_DESCRIPTOR).isEqualTo(fmBandDescriptorCompared);
}
@Test
@@ -258,6 +332,44 @@
}
@Test
+ public void hashCode_withSameFmBandDescriptors_equals() {
+ RadioManager.FmBandDescriptor fmBandDescriptorCompared = createFmBandDescriptor();
+
+ assertWithMessage("Hash code of the same FM Band Descriptor")
+ .that(fmBandDescriptorCompared.hashCode()).isEqualTo(FM_BAND_DESCRIPTOR.hashCode());
+ }
+
+ @Test
+ public void hashCode_withSameAmBandDescriptors_equals() {
+ RadioManager.AmBandDescriptor amBandDescriptorCompared = createAmBandDescriptor();
+
+ assertWithMessage("Hash code of the same AM Band Descriptor")
+ .that(amBandDescriptorCompared.hashCode()).isEqualTo(AM_BAND_DESCRIPTOR.hashCode());
+ }
+
+ @Test
+ public void hashCode_withFmBandDescriptorsOfDifferentAfSupports_notEquals() {
+ RadioManager.FmBandDescriptor fmBandDescriptorCompared = new RadioManager.FmBandDescriptor(
+ REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, FM_UPPER_LIMIT, FM_SPACING,
+ STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, !AF_SUPPORTED, EA_SUPPORTED);
+
+ assertWithMessage("Hash code of FM Band Descriptor of different spacing")
+ .that(fmBandDescriptorCompared.hashCode())
+ .isNotEqualTo(FM_BAND_DESCRIPTOR.hashCode());
+ }
+
+ @Test
+ public void hashCode_withAmBandDescriptorsOfDifferentSpacings_notEquals() {
+ RadioManager.AmBandDescriptor amBandDescriptorCompared =
+ new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+ AM_UPPER_LIMIT, AM_SPACING * 2, STEREO_SUPPORTED);
+
+ assertWithMessage("Hash code of AM Band Descriptor of different spacing")
+ .that(amBandDescriptorCompared.hashCode())
+ .isNotEqualTo(AM_BAND_DESCRIPTOR.hashCode());
+ }
+
+ @Test
public void getType_forBandConfig() {
RadioManager.BandConfig fmBandConfig = createFmBandConfig();
@@ -298,8 +410,24 @@
}
@Test
+ public void describeContents_forBandConfig() {
+ RadioManager.BandConfig bandConfig = createFmBandConfig();
+
+ assertWithMessage("FM Band Config contents")
+ .that(bandConfig.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forBandConfigCreator() {
+ RadioManager.BandConfig[] bandConfigs =
+ RadioManager.BandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Band Configs").that(bandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void getStereo_forFmBandConfig() {
- assertWithMessage("FM Band Config stereo ")
+ assertWithMessage("FM Band Config stereo")
.that(FM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
}
@@ -328,12 +456,66 @@
}
@Test
+ public void describeContents_forFmBandConfig() {
+ assertWithMessage("FM Band Config contents")
+ .that(FM_BAND_CONFIG.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel_forFmBandConfig() {
+ Parcel parcel = Parcel.obtain();
+
+ FM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioManager.FmBandConfig fmBandConfigFromParcel =
+ RadioManager.FmBandConfig.CREATOR.createFromParcel(parcel);
+ assertWithMessage("FM Band Config created from parcel")
+ .that(fmBandConfigFromParcel).isEqualTo(FM_BAND_CONFIG);
+ }
+
+ @Test
+ public void newArray_forFmBandConfigCreator() {
+ RadioManager.FmBandConfig[] fmBandConfigs =
+ RadioManager.FmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("FM Band Configs").that(fmBandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void getStereo_forAmBandConfig() {
assertWithMessage("AM Band Config stereo")
.that(AM_BAND_CONFIG.getStereo()).isEqualTo(STEREO_SUPPORTED);
}
@Test
+ public void describeContents_forAmBandConfig() {
+ assertWithMessage("AM Band Config contents")
+ .that(AM_BAND_CONFIG.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void writeToParcel_forAmBandConfig() {
+ Parcel parcel = Parcel.obtain();
+
+ AM_BAND_CONFIG.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioManager.AmBandConfig amBandConfigFromParcel =
+ RadioManager.AmBandConfig.CREATOR.createFromParcel(parcel);
+ assertWithMessage("AM Band Config created from parcel")
+ .that(amBandConfigFromParcel).isEqualTo(AM_BAND_CONFIG);
+ }
+
+ @Test
+ public void newArray_forAmBandConfigCreator() {
+ RadioManager.AmBandConfig[] amBandConfigs =
+ RadioManager.AmBandConfig.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("AM Band Configs").that(amBandConfigs).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void equals_withSameFmBandConfigs_returnsTrue() {
RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
@@ -387,6 +569,43 @@
}
@Test
+ public void hashCode_withSameFmBandConfigs_equals() {
+ RadioManager.FmBandConfig fmBandConfigCompared = createFmBandConfig();
+
+ assertWithMessage("Hash code of the same FM Band Config")
+ .that(FM_BAND_CONFIG.hashCode()).isEqualTo(fmBandConfigCompared.hashCode());
+ }
+
+ @Test
+ public void hashCode_withSameAmBandConfigs_equals() {
+ RadioManager.AmBandConfig amBandConfigCompared = createAmBandConfig();
+
+ assertWithMessage("Hash code of the same AM Band Config")
+ .that(amBandConfigCompared.hashCode()).isEqualTo(AM_BAND_CONFIG.hashCode());
+ }
+
+ @Test
+ public void hashCode_withFmBandConfigsOfDifferentTypes_notEquals() {
+ RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig(
+ new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM_HD, FM_LOWER_LIMIT,
+ FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED,
+ AF_SUPPORTED, EA_SUPPORTED));
+
+ assertWithMessage("Hash code of FM Band Config with different type")
+ .that(fmBandConfigCompared.hashCode()).isNotEqualTo(FM_BAND_CONFIG.hashCode());
+ }
+
+ @Test
+ public void hashCode_withAmBandConfigsOfDifferentStereoSupports_notEquals() {
+ RadioManager.AmBandConfig amBandConfigCompared = new RadioManager.AmBandConfig(
+ new RadioManager.AmBandDescriptor(REGION, RadioManager.BAND_AM, AM_LOWER_LIMIT,
+ AM_UPPER_LIMIT, AM_SPACING, !STEREO_SUPPORTED));
+
+ assertWithMessage("Hash code of AM Band Config with different stereo support")
+ .that(amBandConfigCompared.hashCode()).isNotEqualTo(AM_BAND_CONFIG.hashCode());
+ }
+
+ @Test
public void getId_forModuleProperties() {
assertWithMessage("Properties id")
.that(AMFM_PROPERTIES.getId()).isEqualTo(PROPERTIES_ID);
@@ -509,6 +728,12 @@
}
@Test
+ public void describeContents_forModuleProperties() {
+ assertWithMessage("Module properties contents")
+ .that(AMFM_PROPERTIES.describeContents()).isEqualTo(0);
+ }
+
+ @Test
public void equals_withSameProperties_returnsTrue() {
RadioManager.ModuleProperties propertiesCompared = createAmFmProperties();
@@ -530,6 +755,23 @@
}
@Test
+ public void hashCode_withSameModuleProperties_equals() {
+ RadioManager.ModuleProperties propertiesCompared = createAmFmProperties();
+
+ assertWithMessage("Hash code of the same module properties")
+ .that(propertiesCompared.hashCode()).isEqualTo(AMFM_PROPERTIES.hashCode());
+ }
+
+ @Test
+ public void newArray_forModulePropertiesCreator() {
+ RadioManager.ModuleProperties[] modulePropertiesArray =
+ RadioManager.ModuleProperties.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Module properties array")
+ .that(modulePropertiesArray).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
public void getSelector_forProgramInfo() {
assertWithMessage("Selector of DAB program info")
.that(DAB_PROGRAM_INFO.getSelector()).isEqualTo(DAB_SELECTOR);
@@ -549,7 +791,7 @@
@Test
public void getRelatedContent_forProgramInfo() {
- assertWithMessage("Related contents of DAB program info")
+ assertWithMessage("DAB program info contents")
.that(DAB_PROGRAM_INFO.getRelatedContent())
.containsExactly(DAB_SID_EXT_IDENTIFIER_RELATED);
}
@@ -627,6 +869,33 @@
}
@Test
+ public void describeContents_forProgramInfo() {
+ assertWithMessage("Program info contents")
+ .that(DAB_PROGRAM_INFO.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forProgramInfoCreator() {
+ RadioManager.ProgramInfo[] programInfoArray =
+ RadioManager.ProgramInfo.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Program infos").that(programInfoArray).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void writeToParcel_forProgramInfo() {
+ Parcel parcel = Parcel.obtain();
+
+ DAB_PROGRAM_INFO.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioManager.ProgramInfo programInfoFromParcel =
+ RadioManager.ProgramInfo.CREATOR.createFromParcel(parcel);
+ assertWithMessage("Program info created from parcel")
+ .that(programInfoFromParcel).isEqualTo(DAB_PROGRAM_INFO);
+ }
+
+ @Test
public void equals_withSameProgramInfo_returnsTrue() {
RadioManager.ProgramInfo dabProgramInfoCompared = createDabProgramInfo(DAB_SELECTOR);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
index fe15597..5771135 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioMetadataTest.java
@@ -20,18 +20,63 @@
import static org.junit.Assert.assertThrows;
+import android.graphics.Bitmap;
import android.hardware.radio.RadioMetadata;
+import android.os.Parcel;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
import java.util.Set;
+@RunWith(MockitoJUnitRunner.class)
public final class RadioMetadataTest {
+ private static final int CREATOR_ARRAY_SIZE = 3;
private static final int INT_KEY_VALUE = 1;
+ private static final long TEST_UTC_SECOND_SINCE_EPOCH = 200;
+ private static final int TEST_TIME_ZONE_OFFSET_MINUTES = 1;
private final RadioMetadata.Builder mBuilder = new RadioMetadata.Builder();
+ @Mock
+ private Bitmap mBitmapValue;
+
+ @Test
+ public void describeContents_forClock() {
+ RadioMetadata.Clock clock = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH,
+ TEST_TIME_ZONE_OFFSET_MINUTES);
+
+ assertWithMessage("Describe contents for metadata clock")
+ .that(clock.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forClockCreator() {
+ RadioMetadata.Clock[] clocks = RadioMetadata.Clock.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Clock array size").that(clocks.length).isEqualTo(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void writeToParcel_forClock() {
+ RadioMetadata.Clock clockExpected = new RadioMetadata.Clock(TEST_UTC_SECOND_SINCE_EPOCH,
+ TEST_TIME_ZONE_OFFSET_MINUTES);
+ Parcel parcel = Parcel.obtain();
+
+ clockExpected.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioMetadata.Clock clockFromParcel = RadioMetadata.Clock.CREATOR.createFromParcel(parcel);
+ assertWithMessage("UTC second since epoch of metadata clock created from parcel")
+ .that(clockFromParcel.getUtcEpochSeconds()).isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH);
+ assertWithMessage("Time zone offset minutes of metadata clock created from parcel")
+ .that(clockFromParcel.getTimezoneOffsetMinutes())
+ .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES);
+ }
+
@Test
public void putString_withIllegalKey() {
String invalidStringKey = RadioMetadata.METADATA_KEY_RDS_PI;
@@ -129,22 +174,56 @@
}
@Test
+ public void getBitmap_withKeyInMetadata() {
+ String key = RadioMetadata.METADATA_KEY_ICON;
+ RadioMetadata metadata = mBuilder.putBitmap(key, mBitmapValue).build();
+
+ assertWithMessage("Bitmap value for key %s in metadata", key)
+ .that(metadata.getBitmap(key)).isEqualTo(mBitmapValue);
+ }
+
+ @Test
+ public void getBitmap_withKeyNotInMetadata() {
+ String key = RadioMetadata.METADATA_KEY_ICON;
+ RadioMetadata metadata = mBuilder.build();
+
+ assertWithMessage("Bitmap value for key %s not in metadata", key)
+ .that(metadata.getBitmap(key)).isNull();
+ }
+
+ @Test
+ public void getBitmapId_withKeyInMetadata() {
+ String key = RadioMetadata.METADATA_KEY_ART;
+ RadioMetadata metadata = mBuilder.putInt(key, INT_KEY_VALUE).build();
+
+ assertWithMessage("Bitmap id value for key %s in metadata", key)
+ .that(metadata.getBitmapId(key)).isEqualTo(INT_KEY_VALUE);
+ }
+
+ @Test
+ public void getBitmapId_withKeyNotInMetadata() {
+ String key = RadioMetadata.METADATA_KEY_ART;
+ RadioMetadata metadata = mBuilder.build();
+
+ assertWithMessage("Bitmap id value for key %s not in metadata", key)
+ .that(metadata.getBitmapId(key)).isEqualTo(0);
+ }
+
+ @Test
public void getClock_withKeyInMetadata() {
String key = RadioMetadata.METADATA_KEY_CLOCK;
- long utcSecondsSinceEpochExpected = 200;
- int timezoneOffsetMinutesExpected = 1;
RadioMetadata metadata = mBuilder
- .putClock(key, utcSecondsSinceEpochExpected, timezoneOffsetMinutesExpected)
+ .putClock(key, TEST_UTC_SECOND_SINCE_EPOCH, TEST_TIME_ZONE_OFFSET_MINUTES)
.build();
RadioMetadata.Clock clockExpected = metadata.getClock(key);
assertWithMessage("Number of seconds since epoch of value for key %s in metadata", key)
.that(clockExpected.getUtcEpochSeconds())
- .isEqualTo(utcSecondsSinceEpochExpected);
+ .isEqualTo(TEST_UTC_SECOND_SINCE_EPOCH);
assertWithMessage("Offset of timezone in minutes of value for key %s in metadata", key)
.that(clockExpected.getTimezoneOffsetMinutes())
- .isEqualTo(timezoneOffsetMinutesExpected);
+ .isEqualTo(TEST_TIME_ZONE_OFFSET_MINUTES);
}
@Test
@@ -180,12 +259,13 @@
RadioMetadata metadata = mBuilder
.putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+ .putBitmap(RadioMetadata.METADATA_KEY_ICON, mBitmapValue)
.build();
Set<String> metadataSet = metadata.keySet();
assertWithMessage("Metadata set of non-empty metadata")
- .that(metadataSet).containsExactly(
+ .that(metadataSet).containsExactly(RadioMetadata.METADATA_KEY_ICON,
RadioMetadata.METADATA_KEY_RDS_PI, RadioMetadata.METADATA_KEY_ARTIST);
}
@@ -208,4 +288,46 @@
.that(key).isEqualTo(RadioMetadata.METADATA_KEY_RDS_PI);
}
+ @Test
+ public void equals_forMetadataWithSameContents_returnsTrue() {
+ RadioMetadata metadata = mBuilder
+ .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
+ .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+ .build();
+ RadioMetadata.Builder copyBuilder = new RadioMetadata.Builder(metadata);
+ RadioMetadata metadataCopied = copyBuilder.build();
+
+ assertWithMessage("Metadata with the same contents")
+ .that(metadataCopied).isEqualTo(metadata);
+ }
+
+ @Test
+ public void describeContents_forMetadata() {
+ RadioMetadata metadata = mBuilder.build();
+
+ assertWithMessage("Metadata contents").that(metadata.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void newArray_forRadioMetadataCreator() {
+ RadioMetadata[] metadataArray = RadioMetadata.CREATOR.newArray(CREATOR_ARRAY_SIZE);
+
+ assertWithMessage("Radio metadata array").that(metadataArray).hasLength(CREATOR_ARRAY_SIZE);
+ }
+
+ @Test
+ public void writeToParcel_forRadioMetadata() {
+ RadioMetadata metadataExpected = mBuilder
+ .putInt(RadioMetadata.METADATA_KEY_RDS_PI, INT_KEY_VALUE)
+ .putString(RadioMetadata.METADATA_KEY_ARTIST, "artistTest")
+ .build();
+ Parcel parcel = Parcel.obtain();
+
+ metadataExpected.writeToParcel(parcel, /* flags= */ 0);
+ parcel.setDataPosition(0);
+
+ RadioMetadata metadataFromParcel = RadioMetadata.CREATOR.createFromParcel(parcel);
+ assertWithMessage("Radio metadata created from parcel")
+ .that(metadataFromParcel).isEqualTo(metadataExpected);
+ }
}
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
index bb1a3b18..ee1e10f 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityManagerTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.when;
import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IAccessibilityServiceClient;
import android.app.Instrumentation;
import android.app.PendingIntent;
import android.app.RemoteAction;
@@ -34,6 +35,7 @@
import android.graphics.drawable.Icon;
import android.os.UserHandle;
+import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -51,6 +53,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executors;
/**
* Tests for the AccessibilityManager by mocking the backing service.
@@ -70,6 +73,7 @@
LABEL,
DESCRIPTION,
TEST_PENDING_INTENT);
+ private static final int DISPLAY_ID = 22;
@Mock private IAccessibilityManager mMockService;
private MessageCapturingHandler mHandler;
@@ -224,4 +228,45 @@
assertEquals(mFocusColorDefaultValue,
manager.getAccessibilityFocusColor());
}
+
+ @Test
+ public void testRegisterAccessibilityProxy() throws Exception {
+ // Accessibility does not need to be enabled for a proxy to be registered.
+ AccessibilityManager manager =
+ new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
+ UserHandle.USER_CURRENT, true);
+
+
+ ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
+ infos.add(new AccessibilityServiceInfo());
+ AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
+ manager.registerDisplayProxy(proxy);
+ // Cannot access proxy.mServiceClient directly due to visibility.
+ verify(mMockService).registerProxyForDisplay(any(IAccessibilityServiceClient.class),
+ any(Integer.class));
+ }
+
+ @Test
+ public void testUnregisterAccessibilityProxy() throws Exception {
+ // Accessibility does not need to be enabled for a proxy to be registered.
+ final AccessibilityManager manager =
+ new AccessibilityManager(mInstrumentation.getContext(), mHandler, mMockService,
+ UserHandle.USER_CURRENT, true);
+
+ final ArrayList<AccessibilityServiceInfo> infos = new ArrayList<>();
+ infos.add(new AccessibilityServiceInfo());
+
+ final AccessibilityDisplayProxy proxy = new MyAccessibilityProxy(DISPLAY_ID, infos);
+ manager.registerDisplayProxy(proxy);
+ manager.unregisterDisplayProxy(proxy);
+ verify(mMockService).unregisterProxyForDisplay(proxy.getDisplayId());
+ }
+
+ private class MyAccessibilityProxy extends AccessibilityDisplayProxy {
+ // TODO(241429275): Will override A11yProxy methods in the future.
+ MyAccessibilityProxy(int displayId,
+ @NonNull List<AccessibilityServiceInfo> serviceInfos) {
+ super(displayId, Executors.newSingleThreadExecutor(), serviceInfos);
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
new file mode 100644
index 0000000..8779cc0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_desktop_button.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+>
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="6.0"
+ android:translateY="6.0">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M5.958,37.708Q4.458,37.708 3.354,36.604Q2.25,35.5 2.25,34V18.292Q2.25,16.792 3.354,15.688Q4.458,14.583 5.958,14.583H9.5V5.958Q9.5,4.458 10.625,3.354Q11.75,2.25 13.208,2.25H34Q35.542,2.25 36.646,3.354Q37.75,4.458 37.75,5.958V21.667Q37.75,23.167 36.646,24.271Q35.542,25.375 34,25.375H30.5V34Q30.5,35.5 29.396,36.604Q28.292,37.708 26.792,37.708ZM5.958,34H26.792Q26.792,34 26.792,34Q26.792,34 26.792,34V21.542H5.958V34Q5.958,34 5.958,34Q5.958,34 5.958,34ZM30.5,21.667H34Q34,21.667 34,21.667Q34,21.667 34,21.667V9.208H13.208V14.583H26.833Q28.375,14.583 29.438,15.667Q30.5,16.75 30.5,18.25Z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
new file mode 100644
index 0000000..ea0fbb0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_floating_button.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+>
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="6.0"
+ android:translateY="6.0">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M18.167,21.875H29.833V10.208H18.167ZM7.875,35.833Q6.375,35.833 5.271,34.729Q4.167,33.625 4.167,32.125V7.875Q4.167,6.375 5.271,5.271Q6.375,4.167 7.875,4.167H32.125Q33.625,4.167 34.729,5.271Q35.833,6.375 35.833,7.875V32.125Q35.833,33.625 34.729,34.729Q33.625,35.833 32.125,35.833ZM7.875,32.125H32.125Q32.125,32.125 32.125,32.125Q32.125,32.125 32.125,32.125V7.875Q32.125,7.875 32.125,7.875Q32.125,7.875 32.125,7.875H7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125ZM7.875,7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875V32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125Q7.875,32.125 7.875,32.125V7.875Q7.875,7.875 7.875,7.875Q7.875,7.875 7.875,7.875Z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
new file mode 100644
index 0000000..c55cbe2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_fullscreen_button.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+>
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="6.0"
+ android:translateY="6.0">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M34.042,14.625V9.333Q34.042,9.333 34.042,9.333Q34.042,9.333 34.042,9.333H28.708V5.708H33.917Q35.458,5.708 36.562,6.833Q37.667,7.958 37.667,9.458V14.625ZM2.375,14.625V9.458Q2.375,7.958 3.479,6.833Q4.583,5.708 6.125,5.708H11.292V9.333H6Q6,9.333 6,9.333Q6,9.333 6,9.333V14.625ZM28.708,34.25V30.667H34.042Q34.042,30.667 34.042,30.667Q34.042,30.667 34.042,30.667V25.333H37.667V30.542Q37.667,32 36.562,33.125Q35.458,34.25 33.917,34.25ZM6.125,34.25Q4.583,34.25 3.479,33.125Q2.375,32 2.375,30.542V25.333H6V30.667Q6,30.667 6,30.667Q6,30.667 6,30.667H11.292V34.25ZM9.333,27.292V12.667H30.708V27.292ZM12.917,23.708H27.125V16.25H12.917ZM12.917,23.708V16.25V23.708Z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_more_button.xml b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
new file mode 100644
index 0000000..447df43
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_more_button.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+>
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="6.0"
+ android:translateY="6.0">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M8.083,22.833Q6.917,22.833 6.104,22Q5.292,21.167 5.292,20Q5.292,18.833 6.125,18Q6.958,17.167 8.125,17.167Q9.292,17.167 10.125,18Q10.958,18.833 10.958,20Q10.958,21.167 10.125,22Q9.292,22.833 8.083,22.833ZM20,22.833Q18.833,22.833 18,22Q17.167,21.167 17.167,20Q17.167,18.833 18,18Q18.833,17.167 20,17.167Q21.167,17.167 22,18Q22.833,18.833 22.833,20Q22.833,21.167 22,22Q21.167,22.833 20,22.833ZM31.875,22.833Q30.708,22.833 29.875,22Q29.042,21.167 29.042,20Q29.042,18.833 29.875,18Q30.708,17.167 31.917,17.167Q33.083,17.167 33.896,18Q34.708,18.833 34.708,20Q34.708,21.167 33.875,22Q33.042,22.833 31.875,22.833Z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
new file mode 100644
index 0000000..c334a54
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/caption_split_screen_button.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32.0dp"
+ android:height="32.0dp"
+ android:viewportWidth="32.0"
+ android:viewportHeight="32.0"
+>
+ <group android:translateX="6.0"
+ android:translateY="8.0">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M18 14L13 14L13 2L18 2L18 14ZM20 14L20 2C20 0.9 19.1 -3.93402e-08 18 -8.74228e-08L13 -3.0598e-07C11.9 -3.54062e-07 11 0.9 11 2L11 14C11 15.1 11.9 16 13 16L18 16C19.1 16 20 15.1 20 14ZM7 14L2 14L2 2L7 2L7 14ZM9 14L9 2C9 0.9 8.1 -5.20166e-07 7 -5.68248e-07L2 -7.86805e-07C0.9 -8.34888e-07 -3.93403e-08 0.9 -8.74228e-08 2L-6.11959e-07 14C-6.60042e-07 15.1 0.9 16 2 16L7 16C8.1 16 9 15.1 9 14Z"/> </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml
new file mode 100644
index 0000000..e307f00
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/handle_menu_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="210.0dp"
+ android:height="64.0dp"
+ android:tint="@color/decor_button_light_color"
+>
+ <group android:scaleX="0.5"
+ android:scaleY="0.5"
+ android:translateX="8.0"
+ android:translateY="8.0" >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M18.3334 14L13.3334 14L13.3334 2L18.3334 2L18.3334 14ZM20.3334 14L20.3334 2C20.3334 0.9 19.4334 -3.93402e-08 18.3334 -8.74228e-08L13.3334 -3.0598e-07C12.2334 -3.54062e-07 11.3334 0.9 11.3334 2L11.3334 14C11.3334 15.1 12.2334 16 13.3334 16L18.3334 16C19.4334 16 20.3334 15.1 20.3334 14ZM7.33337 14L2.33337 14L2.33337 2L7.33337 2L7.33337 14ZM9.33337 14L9.33337 2C9.33337 0.899999 8.43337 -5.20166e-07 7.33337 -5.68248e-07L2.33337 -7.86805e-07C1.23337 -8.34888e-07 0.333374 0.899999 0.333374 2L0.333373 14C0.333373 15.1 1.23337 16 2.33337 16L7.33337 16C8.43337 16 9.33337 15.1 9.33337 14Z"/>
+ </group>
+</vector>
diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
new file mode 100644
index 0000000..d9a140b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<com.android.wm.shell.windowdecor.WindowDecorLinearLayout
+xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/handle_menu"
+android:layout_width="wrap_content"
+android:layout_height="wrap_content"
+android:gravity="center_horizontal"
+android:background="@drawable/decor_caption_title">
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/fullscreen_button"
+ android:contentDescription="@string/fullscreen_text"
+ android:background="@drawable/caption_fullscreen_button"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/split_screen_button"
+ android:contentDescription="@string/split_screen_text"
+ android:background="@drawable/caption_split_screen_button"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/floating_button"
+ android:contentDescription="@string/float_button_text"
+ android:background="@drawable/caption_floating_button"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/desktop_button"
+ android:contentDescription="@string/desktop_text"
+ android:background="@drawable/caption_desktop_button"/>
+ <Button
+ style="@style/CaptionButtonStyle"
+ android:id="@+id/more_button"
+ android:contentDescription="@string/more_button_text"
+ android:background="@drawable/caption_more_button"/>
+</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
index 38cd570..51e634c 100644
--- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
+++ b/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml
@@ -19,14 +19,10 @@
android:id="@+id/caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="center_horizontal"
android:background="@drawable/decor_caption_title">
<Button
+ style="@style/CaptionButtonStyle"
android:id="@+id/back_button"
- android:layout_width="32dp"
- android:layout_height="32dp"
- android:layout_margin="5dp"
- android:padding="4dp"
android:contentDescription="@string/back_button_text"
android:background="@drawable/decor_back_button_dark"
/>
@@ -39,11 +35,8 @@
android:contentDescription="@string/handle_text"
android:background="@drawable/decor_handle_dark"/>
<Button
+ style="@style/CaptionButtonStyle"
android:id="@+id/close_window"
- android:layout_width="32dp"
- android:layout_height="32dp"
- android:layout_margin="5dp"
- android:padding="4dp"
android:contentDescription="@string/close_button_text"
android:background="@drawable/decor_close_button_dark"/>
</com.android.wm.shell.windowdecor.WindowDecorLinearLayout>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 4807f08..097a567 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -202,4 +202,14 @@
<string name="back_button_text">Back</string>
<!-- Accessibility text for the caption handle [CHAR LIMIT=NONE] -->
<string name="handle_text">Handle</string>
+ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
+ <string name="fullscreen_text">Fullscreen</string>
+ <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
+ <string name="desktop_text">Desktop Mode</string>
+ <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] -->
+ <string name="split_screen_text">Split Screen</string>
+ <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] -->
+ <string name="more_button_text">More</string>
+ <!-- Accessibility text for the handle floating window button [CHAR LIMIT=NONE] -->
+ <string name="float_button_text">Float</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 19f7c3e..a859721 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -30,6 +30,13 @@
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
+ <style name="CaptionButtonStyle">
+ <item name="android:layout_width">32dp</item>
+ <item name="android:layout_height">32dp</item>
+ <item name="android:layout_margin">5dp</item>
+ <item name="android:padding">4dp</item>
+ </style>
+
<style name="DockedDividerBackground">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/split_divider_bar_width</item>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
new file mode 100644
index 0000000..7237d2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-modules splitscreen owner
+chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index f1670cd..4459f57 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -27,6 +27,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.TaskViewTransitions;
@@ -271,11 +272,12 @@
TaskStackListenerImpl taskStackListener,
UiEventLogger uiEventLogger,
InteractionJankMonitor jankMonitor,
+ RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
@ShellMainThread ShellExecutor mainExecutor,
@ShellMainThread Handler mainHandler) {
return OneHandedController.create(context, shellInit, shellCommandHandler, shellController,
windowManager, displayController, displayLayout, taskStackListener, jankMonitor,
- uiEventLogger, mainExecutor, mainHandler);
+ uiEventLogger, rootDisplayAreaOrganizer, mainExecutor, mainHandler);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
index b310ee2..b5ed509 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
@@ -34,6 +34,7 @@
import android.os.Binder;
import android.util.Slog;
import android.view.ContextThemeWrapper;
+import android.view.Display;
import android.view.IWindow;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
@@ -47,6 +48,7 @@
import androidx.annotation.Nullable;
import com.android.wm.shell.R;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayLayout;
import java.io.PrintWriter;
@@ -67,11 +69,14 @@
private SurfaceControl mLeash;
private View mBackgroundView;
private @OneHandedState.State int mCurrentState;
+ private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
- public BackgroundWindowManager(Context context) {
+ public BackgroundWindowManager(Context context,
+ RootDisplayAreaOrganizer rootDisplayAreaOrganizer) {
super(context.getResources().getConfiguration(), null /* rootSurface */,
null /* hostInputToken */);
mContext = context;
+ mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
mTransactionFactory = SurfaceControl.Transaction::new;
}
@@ -112,6 +117,7 @@
.setOpaque(true)
.setName(TAG)
.setCallsite("BackgroundWindowManager#attachToParentSurface");
+ mRootDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, builder);
mLeash = builder.build();
b.setParent(mLeash);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 679d4ca..ad135d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -47,6 +47,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.wm.shell.R;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
@@ -204,12 +205,14 @@
DisplayController displayController, DisplayLayout displayLayout,
TaskStackListenerImpl taskStackListener,
InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger,
+ RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
ShellExecutor mainExecutor, Handler mainHandler) {
OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil();
OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context);
OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor);
OneHandedState oneHandedState = new OneHandedState();
- BackgroundWindowManager backgroundWindowManager = new BackgroundWindowManager(context);
+ BackgroundWindowManager backgroundWindowManager =
+ new BackgroundWindowManager(context, rootDisplayAreaOrganizer);
OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context,
settingsUtil, windowManager, backgroundWindowManager);
OneHandedAnimationController animationController =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index dca516a..36dd8ed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -26,11 +26,16 @@
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
+import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseArray;
import android.view.Choreographer;
+import android.view.InputChannel;
import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -64,8 +69,11 @@
private final SyncTransactionQueue mSyncQueue;
private FreeformTaskTransitionStarter mTransitionStarter;
private DesktopModeController mDesktopModeController;
+ private EventReceiver mEventReceiver;
+ private InputMonitor mInputMonitor;
private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>();
+ private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
public CaptionWindowDecorViewModel(
Context context,
@@ -108,12 +116,19 @@
mSyncQueue);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
+ TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration,
+ mDragStartListener);
CaptionTouchEventListener touchEventListener =
new CaptionTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
windowDecoration.setDragResizeCallback(taskPositioner);
setupWindowDecorationForTransition(taskInfo, startT, finishT);
+ if (mInputMonitor == null) {
+ mInputMonitor = InputManager.getInstance().monitorGestureInput(
+ "caption-touch", mContext.getDisplayId());
+ mEventReceiver = new EventReceiver(
+ mInputMonitor.getInputChannel(), Looper.myLooper());
+ }
return true;
}
@@ -165,6 +180,7 @@
@Override
public void onClick(View v) {
+ CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final int id = v.getId();
if (id == R.id.close_window) {
WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -176,6 +192,15 @@
}
} else if (id == R.id.back_button) {
injectBackKey();
+ } else if (id == R.id.caption_handle) {
+ decoration.createHandleMenu();
+ } else if (id == R.id.desktop_button) {
+ mDesktopModeController.setDesktopModeActive(true);
+ decoration.closeHandleMenu();
+ } else if (id == R.id.fullscreen_button) {
+ mDesktopModeController.setDesktopModeActive(false);
+ decoration.closeHandleMenu();
+ decoration.setButtonVisibility();
}
}
private void injectBackKey() {
@@ -257,6 +282,36 @@
}
}
+ // InputEventReceiver to listen for touch input outside of caption bounds
+ private class EventReceiver extends InputEventReceiver {
+ EventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper);
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ boolean handled = false;
+ if (event instanceof MotionEvent
+ && ((MotionEvent) event).getActionMasked() == MotionEvent.ACTION_UP) {
+ handled = true;
+ CaptionWindowDecorViewModel.this.handleMotionEvent((MotionEvent) event);
+ }
+ finishInputEvent(event, handled);
+ }
+ }
+
+ // If any input received is outside of caption bounds, turn off handle menu
+ private void handleMotionEvent(MotionEvent ev) {
+ int size = mWindowDecorByTaskId.size();
+ for (int i = 0; i < size; i++) {
+ CaptionWindowDecoration decoration = mWindowDecorByTaskId.valueAt(i);
+ if (decoration != null) {
+ decoration.closeHandleMenuIfNeeded(ev);
+ }
+ }
+ }
+
+
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
return DesktopModeStatus.IS_SUPPORTED
@@ -264,4 +319,11 @@
&& mDisplayController.getDisplayContext(taskInfo.displayId)
.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
+
+ private class DragStartListenerImpl implements TaskPositioner.DragStartListener{
+ @Override
+ public void onDragStart(int taskId) {
+ mWindowDecorByTaskId.get(taskId).closeHandleMenu();
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 9d61c14..03cad04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -20,10 +20,14 @@
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.content.res.Resources;
import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Rect;
import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.view.Choreographer;
+import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
@@ -58,6 +62,8 @@
private boolean mDesktopActive;
+ private AdditionalWindow mHandleMenu;
+
CaptionWindowDecoration(
Context context,
DisplayController displayController,
@@ -123,7 +129,20 @@
if (isDragResizeable) {
mRelayoutParams.setOutsets(outsetLeftId, outsetTopId, outsetRightId, outsetBottomId);
}
+ final Resources resources = mDecorWindowContext.getResources();
+ final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
+ final int captionHeight = loadDimensionPixelSize(resources,
+ mRelayoutParams.mCaptionHeightId);
+ final int captionWidth = loadDimensionPixelSize(resources,
+ mRelayoutParams.mCaptionWidthId);
+ final int captionLeft = taskBounds.width() / 2
+ - captionWidth / 2;
+ final int captionTop = taskBounds.top
+ <= captionHeight / 2 ? 0 : -captionHeight / 2;
+ mRelayoutParams.setCaptionPosition(captionLeft, captionTop);
+
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+ taskInfo = null; // Clear it just in case we use it accidentally
mTaskOrganizer.applyTransaction(wct);
@@ -137,15 +156,14 @@
}
// If this task is not focused, do not show caption.
- setCaptionVisibility(taskInfo.isFocused);
+ setCaptionVisibility(mTaskInfo.isFocused);
// Only handle should show if Desktop Mode is inactive.
boolean desktopCurrentStatus = DesktopModeStatus.isActive(mContext);
- if (mDesktopActive != desktopCurrentStatus && taskInfo.isFocused) {
+ if (mDesktopActive != desktopCurrentStatus && mTaskInfo.isFocused) {
mDesktopActive = desktopCurrentStatus;
setButtonVisibility();
}
- taskInfo = null; // Clear it just in case we use it accidentally
if (!isDragResizeable) {
closeDragResizeListener();
@@ -184,9 +202,22 @@
back.setOnClickListener(mOnCaptionButtonClickListener);
View handle = caption.findViewById(R.id.caption_handle);
handle.setOnTouchListener(mOnCaptionTouchListener);
+ handle.setOnClickListener(mOnCaptionButtonClickListener);
setButtonVisibility();
}
+ private void setupHandleMenu() {
+ View menu = mHandleMenu.mWindowViewHost.getView();
+ View fullscreen = menu.findViewById(R.id.fullscreen_button);
+ fullscreen.setOnClickListener(mOnCaptionButtonClickListener);
+ View desktop = menu.findViewById(R.id.desktop_button);
+ desktop.setOnClickListener(mOnCaptionButtonClickListener);
+ View split = menu.findViewById(R.id.split_screen_button);
+ split.setOnClickListener(mOnCaptionButtonClickListener);
+ View more = menu.findViewById(R.id.more_button);
+ more.setOnClickListener(mOnCaptionButtonClickListener);
+ }
+
/**
* Sets caption visibility based on task focus.
*
@@ -194,8 +225,9 @@
*/
private void setCaptionVisibility(boolean visible) {
int v = visible ? View.VISIBLE : View.GONE;
- View caption = mResult.mRootView.findViewById(R.id.caption);
- caption.setVisibility(v);
+ View captionView = mResult.mRootView.findViewById(R.id.caption);
+ captionView.setVisibility(v);
+ if (!visible) closeHandleMenu();
}
/**
@@ -203,6 +235,7 @@
*
*/
public void setButtonVisibility() {
+ mDesktopActive = DesktopModeStatus.isActive(mContext);
int v = mDesktopActive ? View.VISIBLE : View.GONE;
View caption = mResult.mRootView.findViewById(R.id.caption);
View back = caption.findViewById(R.id.back_button);
@@ -220,6 +253,10 @@
caption.getBackground().setTint(v == View.VISIBLE ? Color.WHITE : Color.TRANSPARENT);
}
+ public boolean isHandleMenuActive() {
+ return mHandleMenu != null;
+ }
+
private void closeDragResizeListener() {
if (mDragResizeListener == null) {
return;
@@ -228,9 +265,67 @@
mDragResizeListener = null;
}
+ /**
+ * Create and display handle menu window
+ */
+ public void createHandleMenu() {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ final Resources resources = mDecorWindowContext.getResources();
+ int x = mRelayoutParams.mCaptionX;
+ int y = mRelayoutParams.mCaptionY;
+ int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+ int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+ String namePrefix = "Caption Menu";
+ mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t,
+ x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY,
+ width, height);
+ mSyncQueue.runInSync(transaction -> {
+ transaction.merge(t);
+ t.close();
+ });
+ setupHandleMenu();
+ }
+
+ /**
+ * Close the handle menu window
+ */
+ public void closeHandleMenu() {
+ if (!isHandleMenuActive()) return;
+ mHandleMenu.releaseView();
+ mHandleMenu = null;
+ }
+
+ @Override
+ void releaseViews() {
+ closeHandleMenu();
+ super.releaseViews();
+ }
+
+ /**
+ * Close an open handle menu if input is outside of menu coordinates
+ * @param ev the tapped point to compare against
+ * @return
+ */
+ public void closeHandleMenuIfNeeded(MotionEvent ev) {
+ if (mHandleMenu != null) {
+ Point positionInParent = mTaskOrganizer.getRunningTaskInfo(mTaskInfo.taskId)
+ .positionInParent;
+ final Resources resources = mDecorWindowContext.getResources();
+ ev.offsetLocation(-mRelayoutParams.mCaptionX, -mRelayoutParams.mCaptionY);
+ ev.offsetLocation(-positionInParent.x, -positionInParent.y);
+ int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+ int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+ if (!(ev.getX() >= 0 && ev.getY() >= 0
+ && ev.getX() <= width && ev.getY() <= height)) {
+ closeHandleMenu();
+ }
+ }
+ }
+
@Override
public void close() {
closeDragResizeListener();
+ closeHandleMenu();
super.close();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
index 27c1011..f0f2db7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java
@@ -42,14 +42,18 @@
private final Rect mResizeTaskBounds = new Rect();
private int mCtrlType;
+ private DragStartListener mDragStartListener;
- TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration) {
+ TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration,
+ DragStartListener dragStartListener) {
mTaskOrganizer = taskOrganizer;
mWindowDecoration = windowDecoration;
+ mDragStartListener = dragStartListener;
}
@Override
public void onDragResizeStart(int ctrlType, float x, float y) {
+ mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
mCtrlType = ctrlType;
mTaskBoundsAtDragStart.set(
@@ -97,4 +101,12 @@
mTaskOrganizer.applyTransaction(wct);
}
}
+
+ interface DragStartListener {
+ /**
+ * Inform the implementing class that a drag resize has started
+ * @param taskId id of this positioner's {@link WindowDecoration}
+ */
+ void onDragStart(int taskId);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index b314163..7ecb3f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -200,16 +200,17 @@
final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
final Resources resources = mDecorWindowContext.getResources();
- final int decorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
- final int decorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
+ outResult.mDecorContainerOffsetX = -loadDimensionPixelSize(resources, params.mOutsetLeftId);
+ outResult.mDecorContainerOffsetY = -loadDimensionPixelSize(resources, params.mOutsetTopId);
outResult.mWidth = taskBounds.width()
+ loadDimensionPixelSize(resources, params.mOutsetRightId)
- - decorContainerOffsetX;
+ - outResult.mDecorContainerOffsetX;
outResult.mHeight = taskBounds.height()
+ loadDimensionPixelSize(resources, params.mOutsetBottomId)
- - decorContainerOffsetY;
+ - outResult.mDecorContainerOffsetY;
startT.setPosition(
- mDecorationContainerSurface, decorContainerOffsetX, decorContainerOffsetY)
+ mDecorationContainerSurface,
+ outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY)
.setWindowCrop(mDecorationContainerSurface,
outResult.mWidth, outResult.mHeight)
// TODO(b/244455401): Change the z-order when it's better organized
@@ -252,14 +253,11 @@
final int captionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
final int captionWidth = loadDimensionPixelSize(resources, params.mCaptionWidthId);
- //Prevent caption from going offscreen if task is too high up
- final int captionYPos = taskBounds.top <= captionHeight / 2 ? 0 : captionHeight / 2;
-
startT.setPosition(
- mCaptionContainerSurface, -decorContainerOffsetX
- + taskBounds.width() / 2 - captionWidth / 2,
- -decorContainerOffsetY - captionYPos)
- .setWindowCrop(mCaptionContainerSurface, taskBounds.width(), captionHeight)
+ mCaptionContainerSurface,
+ -outResult.mDecorContainerOffsetX + params.mCaptionX,
+ -outResult.mDecorContainerOffsetY + params.mCaptionY)
+ .setWindowCrop(mCaptionContainerSurface, captionWidth, captionHeight)
.show(mCaptionContainerSurface);
if (mCaptionWindowManager == null) {
@@ -292,7 +290,7 @@
// Caption insets
mCaptionInsetsRect.set(taskBounds);
mCaptionInsetsRect.bottom =
- mCaptionInsetsRect.top + captionHeight - captionYPos;
+ mCaptionInsetsRect.top + captionHeight + params.mCaptionY;
wct.addRectInsetsProvider(mTaskInfo.token, mCaptionInsetsRect,
CAPTION_INSETS_TYPES);
} else {
@@ -302,10 +300,10 @@
// Task surface itself
Point taskPosition = mTaskInfo.positionInParent;
mTaskSurfaceCrop.set(
- decorContainerOffsetX,
- decorContainerOffsetY,
- outResult.mWidth + decorContainerOffsetX,
- outResult.mHeight + decorContainerOffsetY);
+ outResult.mDecorContainerOffsetX,
+ outResult.mDecorContainerOffsetY,
+ outResult.mWidth + outResult.mDecorContainerOffsetX,
+ outResult.mHeight + outResult.mDecorContainerOffsetY);
startT.show(mTaskSurface);
finishT.setPosition(mTaskSurface, taskPosition.x, taskPosition.y)
.setCrop(mTaskSurface, mTaskSurfaceCrop);
@@ -326,7 +324,7 @@
return true;
}
- private void releaseViews() {
+ void releaseViews() {
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
@@ -369,20 +367,60 @@
releaseViews();
}
- private static int loadDimensionPixelSize(Resources resources, int resourceId) {
+ static int loadDimensionPixelSize(Resources resources, int resourceId) {
if (resourceId == Resources.ID_NULL) {
return 0;
}
return resources.getDimensionPixelSize(resourceId);
}
- private static float loadDimension(Resources resources, int resourceId) {
+ static float loadDimension(Resources resources, int resourceId) {
if (resourceId == Resources.ID_NULL) {
return 0;
}
return resources.getDimension(resourceId);
}
+ /**
+ * Create a window associated with this WindowDecoration.
+ * Note that subclass must dispose of this when the task is hidden/closed.
+ * @param layoutId layout to make the window from
+ * @param t the transaction to apply
+ * @param xPos x position of new window
+ * @param yPos y position of new window
+ * @param width width of new window
+ * @param height height of new window
+ * @return
+ */
+ AdditionalWindow addWindow(int layoutId, String namePrefix,
+ SurfaceControl.Transaction t, int xPos, int yPos, int width, int height) {
+ final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
+ SurfaceControl windowSurfaceControl = builder
+ .setName(namePrefix + " of Task=" + mTaskInfo.taskId)
+ .setContainerLayer()
+ .setParent(mDecorationContainerSurface)
+ .build();
+ View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null);
+
+ t.setPosition(
+ windowSurfaceControl, xPos, yPos)
+ .setWindowCrop(windowSurfaceControl, width, height)
+ .show(windowSurfaceControl);
+ final WindowManager.LayoutParams lp =
+ new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+ lp.setTitle("Additional window of Task=" + mTaskInfo.taskId);
+ lp.setTrustedOverlay();
+ WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration,
+ windowSurfaceControl, null /* hostInputToken */);
+ SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory
+ .create(mDecorWindowContext, mDisplay, windowManager);
+ viewHost.setView(v, lp);
+ return new AdditionalWindow(windowSurfaceControl, viewHost,
+ mSurfaceControlTransactionSupplier);
+ }
+
static class RelayoutParams{
RunningTaskInfo mRunningTaskInfo;
int mLayoutResId;
@@ -395,6 +433,9 @@
int mOutsetLeftId;
int mOutsetRightId;
+ int mCaptionX;
+ int mCaptionY;
+
void setOutsets(int leftId, int topId, int rightId, int bottomId) {
mOutsetLeftId = leftId;
mOutsetTopId = topId;
@@ -402,6 +443,11 @@
mOutsetBottomId = bottomId;
}
+ void setCaptionPosition(int left, int top) {
+ mCaptionX = left;
+ mCaptionY = top;
+ }
+
void reset() {
mLayoutResId = Resources.ID_NULL;
mCaptionHeightId = Resources.ID_NULL;
@@ -412,6 +458,9 @@
mOutsetBottomId = Resources.ID_NULL;
mOutsetLeftId = Resources.ID_NULL;
mOutsetRightId = Resources.ID_NULL;
+
+ mCaptionX = 0;
+ mCaptionY = 0;
}
}
@@ -419,10 +468,14 @@
int mWidth;
int mHeight;
T mRootView;
+ int mDecorContainerOffsetX;
+ int mDecorContainerOffsetY;
void reset() {
mWidth = 0;
mHeight = 0;
+ mDecorContainerOffsetX = 0;
+ mDecorContainerOffsetY = 0;
mRootView = null;
}
}
@@ -432,4 +485,41 @@
return new SurfaceControlViewHost(c, d, wmm);
}
}
+
+ /**
+ * Subclass for additional windows associated with this WindowDecoration
+ */
+ static class AdditionalWindow {
+ SurfaceControl mWindowSurface;
+ SurfaceControlViewHost mWindowViewHost;
+ Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+
+ private AdditionalWindow(SurfaceControl surfaceControl,
+ SurfaceControlViewHost surfaceControlViewHost,
+ Supplier<SurfaceControl.Transaction> transactionSupplier) {
+ mWindowSurface = surfaceControl;
+ mWindowViewHost = surfaceControlViewHost;
+ mTransactionSupplier = transactionSupplier;
+ }
+
+ void releaseView() {
+ WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM();
+
+ if (mWindowViewHost != null) {
+ mWindowViewHost.release();
+ mWindowViewHost = null;
+ }
+ windowManager = null;
+ final SurfaceControl.Transaction t = mTransactionSupplier.get();
+ boolean released = false;
+ if (mWindowSurface != null) {
+ t.remove(mWindowSurface);
+ mWindowSurface = null;
+ released = true;
+ }
+ if (released) {
+ t.apply();
+ }
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index f4efc37..1c28c3d 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -6,3 +6,4 @@
lbill@google.com
madym@google.com
hwwang@google.com
+chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index f802539..7546a55 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -80,7 +80,7 @@
transitions { tapl.goHome() }
}
- @FlakyTest
+ @FlakyTest(bugId = 256863309)
@Test
override fun pipLayerReduces() {
testSpec.assertLayers {
@@ -108,14 +108,6 @@
}
}
- @FlakyTest(bugId = 239807171)
- @Test
- override fun pipAppLayerAlwaysVisible() = super.pipAppLayerAlwaysVisible()
-
- @FlakyTest(bugId = 239807171)
- @Test
- override fun pipLayerRemainInsideVisibleBounds() = super.pipLayerRemainInsideVisibleBounds()
-
@Presubmit
@Test
override fun focusChanges() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java
index f3f7067..11948dbf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java
@@ -24,6 +24,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
@@ -41,11 +42,13 @@
private BackgroundWindowManager mBackgroundWindowManager;
@Mock
private DisplayLayout mMockDisplayLayout;
+ @Mock
+ private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mBackgroundWindowManager = new BackgroundWindowManager(mContext);
+ mBackgroundWindowManager = new BackgroundWindowManager(mContext, mRootDisplayAreaOrganizer);
mBackgroundWindowManager.onDisplayChanged(mMockDisplayLayout);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 4d37e5d..15181b1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -35,6 +35,7 @@
import android.app.ActivityManager;
import android.content.Context;
+import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
@@ -64,6 +65,7 @@
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
+import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
@@ -102,12 +104,14 @@
private final List<SurfaceControl.Builder> mMockSurfaceControlBuilders = new ArrayList<>();
private SurfaceControl.Transaction mMockSurfaceControlStartT;
private SurfaceControl.Transaction mMockSurfaceControlFinishT;
+ private SurfaceControl.Transaction mMockSurfaceControlAddWindowT;
private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
@Before
public void setUp() {
mMockSurfaceControlStartT = createMockSurfaceControlTransaction();
mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
+ mMockSurfaceControlAddWindowT = createMockSurfaceControlTransaction();
doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
.create(any(), any(), any());
@@ -227,8 +231,8 @@
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
- verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, -46, 8);
- verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 300, 64);
+ verify(mMockSurfaceControlStartT).setPosition(captionContainerSurface, 20, 40);
+ verify(mMockSurfaceControlStartT).setWindowCrop(captionContainerSurface, 432, 64);
verify(mMockSurfaceControlStartT).show(captionContainerSurface);
verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
@@ -242,7 +246,7 @@
verify(mMockView).setTaskFocusState(true);
verify(mMockWindowContainerTransaction)
.addRectInsetsProvider(taskInfo.token,
- new Rect(100, 300, 400, 332),
+ new Rect(100, 300, 400, 364),
new int[] { InsetsState.ITYPE_CAPTION_BAR });
}
@@ -366,6 +370,71 @@
verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
}
+ @Test
+ public void testAddWindow() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final SurfaceControl decorContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder decorContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(decorContainerSurface);
+ mMockSurfaceControlBuilders.add(decorContainerSurfaceBuilder);
+ final SurfaceControl taskBackgroundSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder taskBackgroundSurfaceBuilder =
+ createMockSurfaceControlBuilder(taskBackgroundSurface);
+ mMockSurfaceControlBuilders.add(taskBackgroundSurfaceBuilder);
+ final SurfaceControl captionContainerSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder captionContainerSurfaceBuilder =
+ createMockSurfaceControlBuilder(captionContainerSurface);
+ mMockSurfaceControlBuilders.add(captionContainerSurfaceBuilder);
+
+ final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+ mMockSurfaceControlTransactions.add(t);
+ final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
+ new ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .build();
+ taskInfo.isFocused = true;
+ taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
+ mRelayoutParams.setOutsets(
+ R.dimen.test_window_decor_left_outset,
+ R.dimen.test_window_decor_top_outset,
+ R.dimen.test_window_decor_right_outset,
+ R.dimen.test_window_decor_bottom_outset);
+ final SurfaceControl taskSurface = mock(SurfaceControl.class);
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo, taskSurface);
+ windowDecor.relayout(taskInfo);
+
+ final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class);
+ final SurfaceControl.Builder additionalWindowSurfaceBuilder =
+ createMockSurfaceControlBuilder(additionalWindowSurface);
+ mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder);
+
+ WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow();
+
+ verify(additionalWindowSurfaceBuilder).setContainerLayer();
+ verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface);
+ verify(additionalWindowSurfaceBuilder).build();
+ verify(mMockSurfaceControlAddWindowT).setPosition(additionalWindowSurface, 20, 40);
+ verify(mMockSurfaceControlAddWindowT).setWindowCrop(additionalWindowSurface, 432, 64);
+ verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface);
+ verify(mMockSurfaceControlViewHostFactory, Mockito.times(2))
+ .create(any(), eq(defaultDisplay), any());
+ assertThat(additionalWindow.mWindowViewHost).isNotNull();
+
+ additionalWindow.releaseView();
+
+ assertThat(additionalWindow.mWindowViewHost).isNull();
+ assertThat(additionalWindow.mWindowSurface).isNull();
+ }
+
private TestWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl testSurface) {
return new TestWindowDecoration(InstrumentationRegistry.getInstrumentation().getContext(),
@@ -429,5 +498,20 @@
relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
+
+ private WindowDecoration.AdditionalWindow addTestWindow() {
+ final Resources resources = mDecorWindowContext.getResources();
+ int x = mRelayoutParams.mCaptionX;
+ int y = mRelayoutParams.mCaptionY;
+ int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId);
+ int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+ String name = "Test Window";
+ WindowDecoration.AdditionalWindow additionalWindow =
+ addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT,
+ x - mRelayoutResult.mDecorContainerOffsetX,
+ y - mRelayoutResult.mDecorContainerOffsetY,
+ width, height);
+ return additionalWindow;
+ }
}
}
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index 95599bd..1183ca3 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -174,6 +174,12 @@
jfieldID typeId;
} gDescriptorInfo;
+static struct {
+ jclass clazz;
+ jmethodID ctorId;
+ jmethodID setId;
+} gBufferInfo;
+
struct fields_t {
jmethodID postEventFromNativeID;
jmethodID lockAndGetContextID;
@@ -460,11 +466,7 @@
return err;
}
- ScopedLocalRef<jclass> clazz(
- env, env->FindClass("android/media/MediaCodec$BufferInfo"));
-
- jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
- env->CallVoidMethod(bufferInfo, method, (jint)offset, (jint)size, timeUs, flags);
+ env->CallVoidMethod(bufferInfo, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags);
return OK;
}
@@ -1091,13 +1093,7 @@
CHECK(msg->findInt64("timeUs", &timeUs));
CHECK(msg->findInt32("flags", (int32_t *)&flags));
- ScopedLocalRef<jclass> clazz(
- env, env->FindClass("android/media/MediaCodec$BufferInfo"));
- jmethodID ctor = env->GetMethodID(clazz.get(), "<init>", "()V");
- jmethodID method = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
-
- obj = env->NewObject(clazz.get(), ctor);
-
+ obj = env->NewObject(gBufferInfo.clazz, gBufferInfo.ctorId);
if (obj == NULL) {
if (env->ExceptionCheck()) {
ALOGE("Could not create MediaCodec.BufferInfo.");
@@ -1107,7 +1103,7 @@
return;
}
- env->CallVoidMethod(obj, method, (jint)offset, (jint)size, timeUs, flags);
+ env->CallVoidMethod(obj, gBufferInfo.setId, (jint)offset, (jint)size, timeUs, flags);
break;
}
@@ -3235,6 +3231,16 @@
gDescriptorInfo.typeId = env->GetFieldID(clazz.get(), "mType", "I");
CHECK(gDescriptorInfo.typeId != NULL);
+
+ clazz.reset(env->FindClass("android/media/MediaCodec$BufferInfo"));
+ CHECK(clazz.get() != NULL);
+ gBufferInfo.clazz = (jclass)env->NewGlobalRef(clazz.get());
+
+ gBufferInfo.ctorId = env->GetMethodID(clazz.get(), "<init>", "()V");
+ CHECK(gBufferInfo.ctorId != NULL);
+
+ gBufferInfo.setId = env->GetMethodID(clazz.get(), "set", "(IIJI)V");
+ CHECK(gBufferInfo.setId != NULL);
}
static void android_media_MediaCodec_native_setup(
diff --git a/packages/CarrierDefaultApp/Android.bp b/packages/CarrierDefaultApp/Android.bp
index 6990ad0..62ffe38 100644
--- a/packages/CarrierDefaultApp/Android.bp
+++ b/packages/CarrierDefaultApp/Android.bp
@@ -13,4 +13,9 @@
libs: ["SliceStore"],
platform_apis: true,
certificate: "platform",
+ optimize: {
+ proguard_flags_files: [
+ "proguard.flags",
+ ],
+ },
}
diff --git a/packages/CarrierDefaultApp/assets/slice_store_test.html b/packages/CarrierDefaultApp/assets/slice_store_test.html
new file mode 100644
index 0000000..7ddbd2d
--- /dev/null
+++ b/packages/CarrierDefaultApp/assets/slice_store_test.html
@@ -0,0 +1,78 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="description" content="
+ This is a HTML page that calls and verifies responses from the @JavascriptInterface functions of
+ SliceStoreWebInterface. Test SliceStore APIs using ADB shell commands and the APIs below:
+
+ FROM TERMINAL:
+ Allow device to override carrier configs:
+ $ adb root
+ Set PREMIUM_CAPABILITY_PRIORITIZE_LATENCY enabled:
+ $ adb shell cmd phone cc set-value -p supported_premium_capabilities_int_array 34
+ Set the carrier purchase URL to this test HTML file:
+ $ adb shell cmd phone cc set-value -p premium_capability_purchase_url_string \
+ file:///android_asset/slice_store_test.html
+ OPTIONAL: Allow premium capability purchase on LTE:
+ $ adb shell cmd phone cc set-value -p premium_capability_supported_on_lte_bool true
+ OPTIONAL: Override ServiceState to fake a NR SA connection:
+ $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --ei data_rat 20
+
+ FROM TEST ACTIVITY:
+ TelephonyManager tm = getApplicationContext().getSystemService(TelephonyManager.class)
+ tm.isPremiumCapabilityAvailable(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY);
+ LinkedBlockingQueue<Integer> purchaseRequests = new LinkedBlockingQueue<>();
+ tm.purchasePremiumCapability(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY,
+ this.getMainExecutor(), request::offer);
+
+ When the test application starts, this HTML will be loaded into the WebView along with the
+ associated JavaScript functions in file:///android_asset/slice_store_test.js.
+ Click on the buttons in the HTML to call the corresponding @JavascriptInterface APIs.
+
+ RESET DEVICE STATE:
+ Clear carrier configurations that were set:
+ $ adb shell cmd phone cc clear-values
+ Clear ServiceState override that was set:
+ $ adb shell am broadcast -a com.android.internal.telephony.TestServiceState --es action reset
+ ">
+ <title>Test SliceStoreActivity</title>
+ <script type="text/javascript" src="slice_store_test.js"></script>
+</head>
+<body>
+ <h1>Test SliceStoreActivity</h1>
+ <h2>Get requested premium capability</h2>
+ <button type="button" onclick="testGetRequestedCapability()">
+ Get requested premium capability
+ </button>
+ <p id="requested_capability"></p>
+
+ <h2>Notify purchase successful</h2>
+ <button type="button" onclick="testNotifyPurchaseSuccessful(60000)">
+ Notify purchase successful for 1 minute
+ </button>
+ <p id="purchase_successful"></p>
+
+ <h2>Notify purchase failed</h2>
+ <button type="button" onclick="testNotifyPurchaseFailed()">
+ Notify purchase failed
+ </button>
+ <p id="purchase_failed"></p>
+</body>
+</html>
diff --git a/packages/CarrierDefaultApp/assets/slice_store_test.js b/packages/CarrierDefaultApp/assets/slice_store_test.js
new file mode 100644
index 0000000..f12a6da
--- /dev/null
+++ b/packages/CarrierDefaultApp/assets/slice_store_test.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function testGetRequestedCapability() {
+ let capability = SliceStoreWebInterface.getRequestedCapability();
+ document.getElementById("requested_capability").innerHTML =
+ "Premium capability requested: " + capability;
+}
+
+function testNotifyPurchaseSuccessful(duration_ms_long = 0) {
+ SliceStoreWebInterface.notifyPurchaseSuccessful(duration);
+ document.getElementById("purchase_successful").innerHTML =
+ "Notified purchase success for duration: " + duration;
+}
+
+function testNotifyPurchaseFailed() {
+ SliceStoreWebInterface.notifyPurchaseFailed();
+ document.getElementById("purchase_failed").innerHTML =
+ "Notified purchase failed.";
+}
diff --git a/packages/CarrierDefaultApp/proguard.flags b/packages/CarrierDefaultApp/proguard.flags
new file mode 100644
index 0000000..64fec2c
--- /dev/null
+++ b/packages/CarrierDefaultApp/proguard.flags
@@ -0,0 +1,4 @@
+# Keep classes and methods that have the @JavascriptInterface annotation
+-keepclassmembers class * {
+ @android.webkit.JavascriptInterface <methods>;
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
index 602e31c..348e389 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreActivity.java
@@ -20,47 +20,63 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.NotificationManager;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
+import android.view.KeyEvent;
import android.webkit.WebView;
import com.android.phone.slicestore.SliceStore;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.concurrent.TimeUnit;
/**
* Activity that launches when the user clicks on the network boost notification.
+ * This will open a {@link WebView} for the carrier website to allow the user to complete the
+ * premium capability purchase.
+ * The carrier website can get the requested premium capability using the JavaScript interface
+ * method {@code SliceStoreWebInterface.getRequestedCapability()}.
+ * If the purchase is successful, the carrier website shall notify SliceStore using the JavaScript
+ * interface method {@code SliceStoreWebInterface.notifyPurchaseSuccessful(duration)}, where
+ * {@code duration} is the duration of the network boost.
+ * If the purchase was not successful, the carrier website shall notify SliceStore using the
+ * JavaScript interface method {@code SliceStoreWebInterface.notifyPurchaseFailed()}.
+ * If either of these notification methods are not called, the purchase cannot be completed
+ * successfully and the purchase request will eventually time out.
*/
public class SliceStoreActivity extends Activity {
private static final String TAG = "SliceStoreActivity";
- private URL mUrl;
- private WebView mWebView;
- private int mPhoneId;
+ private @NonNull WebView mWebView;
+ private @NonNull Context mApplicationContext;
private int mSubId;
- private @TelephonyManager.PremiumCapability int mCapability;
+ @TelephonyManager.PremiumCapability protected int mCapability;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
- mPhoneId = intent.getIntExtra(SliceStore.EXTRA_PHONE_ID,
- SubscriptionManager.INVALID_PHONE_INDEX);
mSubId = intent.getIntExtra(SliceStore.EXTRA_SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mCapability = intent.getIntExtra(SliceStore.EXTRA_PREMIUM_CAPABILITY,
SliceStore.PREMIUM_CAPABILITY_INVALID);
- mUrl = getUrl();
- logd("onCreate: mPhoneId=" + mPhoneId + ", mSubId=" + mSubId + ", mCapability="
+ mApplicationContext = getApplicationContext();
+ URL url = getUrl();
+ logd("onCreate: subId=" + mSubId + ", capability="
+ TelephonyManager.convertPremiumCapabilityToString(mCapability)
- + ", mUrl=" + mUrl);
- getApplicationContext().getSystemService(NotificationManager.class)
+ + ", url=" + url);
+
+ // Cancel network boost notification
+ mApplicationContext.getSystemService(NotificationManager.class)
.cancel(SliceStoreBroadcastReceiver.NETWORK_BOOST_NOTIFICATION_TAG, mCapability);
+
+ // Verify intent and values are valid
if (!SliceStoreBroadcastReceiver.isIntentValid(intent)) {
loge("Not starting SliceStoreActivity with an invalid Intent: " + intent);
SliceStoreBroadcastReceiver.sendSliceStoreResponse(
@@ -68,10 +84,15 @@
finishAndRemoveTask();
return;
}
- if (mUrl == null) {
- loge("Unable to create a URL from carrier configs.");
- SliceStoreBroadcastReceiver.sendSliceStoreResponse(
- intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR);
+ if (url == null) {
+ String error = "Unable to create a URL from carrier configs.";
+ loge(error);
+ Intent data = new Intent();
+ data.putExtra(SliceStore.EXTRA_FAILURE_CODE,
+ SliceStore.FAILURE_CODE_CARRIER_URL_UNAVAILABLE);
+ data.putExtra(SliceStore.EXTRA_FAILURE_REASON, error);
+ SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData(
+ mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_CARRIER_ERROR, data);
finishAndRemoveTask();
return;
}
@@ -83,12 +104,53 @@
return;
}
+ // Create a reference to this activity in SliceStoreBroadcastReceiver
SliceStoreBroadcastReceiver.updateSliceStoreActivity(mCapability, this);
+ // Create and configure WebView
mWebView = new WebView(this);
+ // Enable JavaScript for the carrier purchase website to send results back to SliceStore
+ mWebView.getSettings().setJavaScriptEnabled(true);
+ mWebView.addJavascriptInterface(new SliceStoreWebInterface(this), "SliceStoreWebInterface");
+
+ // Display WebView
setContentView(mWebView);
- mWebView.loadUrl(mUrl.toString());
- // TODO(b/245882601): Get back response from WebView
+ mWebView.loadUrl(url.toString());
+ }
+
+ protected void onPurchaseSuccessful(long duration) {
+ logd("onPurchaseSuccessful: Carrier website indicated successfully purchased premium "
+ + "capability " + TelephonyManager.convertPremiumCapabilityToString(mCapability)
+ + " for " + TimeUnit.MILLISECONDS.toMinutes(duration) + " minutes.");
+ Intent intent = new Intent();
+ intent.putExtra(SliceStore.EXTRA_PURCHASE_DURATION, duration);
+ SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData(
+ mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_SUCCESS, intent);
+ finishAndRemoveTask();
+ }
+
+ protected void onPurchaseFailed(@SliceStore.FailureCode int failureCode,
+ @Nullable String failureReason) {
+ logd("onPurchaseFailed: Carrier website indicated purchase failed for premium capability "
+ + TelephonyManager.convertPremiumCapabilityToString(mCapability) + " with code: "
+ + SliceStore.convertFailureCodeToString(failureCode) + " and reason: "
+ + failureReason);
+ Intent data = new Intent();
+ data.putExtra(SliceStore.EXTRA_FAILURE_CODE, failureCode);
+ data.putExtra(SliceStore.EXTRA_FAILURE_REASON, failureReason);
+ SliceStoreBroadcastReceiver.sendSliceStoreResponseWithData(
+ mApplicationContext, getIntent(), SliceStore.EXTRA_INTENT_CARRIER_ERROR, data);
+ finishAndRemoveTask();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+ // Pressing back in the WebView will go to the previous page instead of closing SliceStore.
+ if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
+ mWebView.goBack();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
}
@Override
@@ -100,8 +162,8 @@
super.onDestroy();
}
- private @Nullable URL getUrl() {
- String url = getApplicationContext().getSystemService(CarrierConfigManager.class)
+ @Nullable private URL getUrl() {
+ String url = mApplicationContext.getSystemService(CarrierConfigManager.class)
.getConfigForSubId(mSubId).getString(
CarrierConfigManager.KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING);
try {
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
index 7eb851d..7867ef1 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreBroadcastReceiver.java
@@ -26,6 +26,7 @@
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
+import android.telephony.AnomalyReporter;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -37,6 +38,7 @@
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
+import java.util.UUID;
/**
* The SliceStoreBroadcastReceiver listens for {@link SliceStore#ACTION_START_SLICE_STORE} from the
@@ -47,6 +49,12 @@
public class SliceStoreBroadcastReceiver extends BroadcastReceiver{
private static final String TAG = "SliceStoreBroadcastReceiver";
+ /**
+ * UUID to report an anomaly when receiving a PendingIntent from an application or process
+ * other than the Phone process.
+ */
+ private static final String UUID_BAD_PENDING_INTENT = "c360246e-95dc-4abf-9dc1-929a76cd7e53";
+
/** Weak references to {@link SliceStoreActivity} for each capability, if it exists. */
private static final Map<Integer, WeakReference<SliceStoreActivity>> sSliceStoreActivities =
new HashMap<>();
@@ -102,6 +110,28 @@
}
/**
+ * Send the PendingIntent containing the corresponding SliceStore response with additional data.
+ *
+ * @param context The Context to use to send the PendingIntent.
+ * @param intent The Intent containing the PendingIntent extra.
+ * @param extra The extra to get the PendingIntent to send.
+ * @param data The Intent containing additional data to send with the PendingIntent.
+ */
+ public static void sendSliceStoreResponseWithData(@NonNull Context context,
+ @NonNull Intent intent, @NonNull String extra, @NonNull Intent data) {
+ PendingIntent pendingIntent = intent.getParcelableExtra(extra, PendingIntent.class);
+ if (pendingIntent == null) {
+ loge("PendingIntent does not exist for extra: " + extra);
+ return;
+ }
+ try {
+ pendingIntent.send(context, 0 /* unused */, data);
+ } catch (PendingIntent.CanceledException e) {
+ loge("Unable to send " + getPendingIntentType(extra) + " intent: " + e);
+ }
+ }
+
+ /**
* Check whether the Intent is valid and can be used to complete purchases in the SliceStore.
* This checks that all necessary extras exist and that the values are valid.
*
@@ -139,7 +169,8 @@
return isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CANCELED)
&& isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_CARRIER_ERROR)
&& isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_REQUEST_FAILED)
- && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA);
+ && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA)
+ && isPendingIntentValid(intent, SliceStore.EXTRA_INTENT_SUCCESS);
}
private static boolean isPendingIntentValid(@NonNull Intent intent, @NonNull String extra) {
@@ -148,12 +179,20 @@
if (pendingIntent == null) {
loge("isPendingIntentValid: " + intentType + " intent not found.");
return false;
- } else if (pendingIntent.getCreatorPackage().equals(TelephonyManager.PHONE_PROCESS_NAME)) {
- return true;
}
- loge("isPendingIntentValid: " + intentType + " intent was created by "
- + pendingIntent.getCreatorPackage() + " instead of the phone process.");
- return false;
+ String creatorPackage = pendingIntent.getCreatorPackage();
+ if (!creatorPackage.equals(TelephonyManager.PHONE_PROCESS_NAME)) {
+ String logStr = "isPendingIntentValid: " + intentType + " intent was created by "
+ + creatorPackage + " instead of the phone process.";
+ loge(logStr);
+ AnomalyReporter.reportAnomaly(UUID.fromString(UUID_BAD_PENDING_INTENT), logStr);
+ return false;
+ }
+ if (!pendingIntent.isBroadcast()) {
+ loge("isPendingIntentValid: " + intentType + " intent is not a broadcast.");
+ return false;
+ }
+ return true;
}
@NonNull private static String getPendingIntentType(@NonNull String extra) {
@@ -162,6 +201,7 @@
case SliceStore.EXTRA_INTENT_CARRIER_ERROR: return "carrier error";
case SliceStore.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
case SliceStore.EXTRA_INTENT_NOT_DEFAULT_DATA: return "not default data";
+ case SliceStore.EXTRA_INTENT_SUCCESS: return "success";
default: {
loge("Unknown pending intent extra: " + extra);
return "unknown(" + extra + ")";
@@ -292,7 +332,6 @@
logd("Closing SliceStore WebView since the user did not complete the purchase "
+ "in time.");
sSliceStoreActivities.get(capability).get().finishAndRemoveTask();
- // TODO: Display a toast to indicate timeout for better UX?
}
}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java
new file mode 100644
index 0000000..ab5d080
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SliceStoreWebInterface.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.carrierdefaultapp;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.webkit.JavascriptInterface;
+
+import com.android.phone.slicestore.SliceStore;
+
+/**
+ * SliceStore web interface class allowing carrier websites to send responses back to SliceStore
+ * using JavaScript.
+ */
+public class SliceStoreWebInterface {
+ @NonNull SliceStoreActivity mActivity;
+
+ public SliceStoreWebInterface(@NonNull SliceStoreActivity activity) {
+ mActivity = activity;
+ }
+ /**
+ * Interface method allowing the carrier website to get the premium capability
+ * that was requested to purchase.
+ *
+ * This can be called using the JavaScript below:
+ * <script type="text/javascript">
+ * function getRequestedCapability(duration) {
+ * SliceStoreWebInterface.getRequestedCapability();
+ * }
+ * </script>
+ */
+ @JavascriptInterface
+ @TelephonyManager.PremiumCapability public int getRequestedCapability() {
+ return mActivity.mCapability;
+ }
+
+ /**
+ * Interface method allowing the carrier website to notify the SliceStore of a successful
+ * premium capability purchase and the duration for which the premium capability is purchased.
+ *
+ * This can be called using the JavaScript below:
+ * <script type="text/javascript">
+ * function notifyPurchaseSuccessful(duration_ms_long = 0) {
+ * SliceStoreWebInterface.notifyPurchaseSuccessful(duration_ms_long);
+ * }
+ * </script>
+ *
+ * @param duration The duration for which the premium capability is purchased in milliseconds.
+ */
+ @JavascriptInterface
+ public void notifyPurchaseSuccessful(long duration) {
+ mActivity.onPurchaseSuccessful(duration);
+ }
+
+ /**
+ * Interface method allowing the carrier website to notify the SliceStore of a failed
+ * premium capability purchase.
+ *
+ * This can be called using the JavaScript below:
+ * <script type="text/javascript">
+ * function notifyPurchaseFailed() {
+ * SliceStoreWebInterface.notifyPurchaseFailed();
+ * }
+ * </script>
+ *
+ * @param failureCode The failure code.
+ * @param failureReason If the failure code is {@link SliceStore#FAILURE_CODE_UNKNOWN},
+ * the human-readable reason for failure.
+ */
+ @JavascriptInterface
+ public void notifyPurchaseFailed(@SliceStore.FailureCode int failureCode,
+ @Nullable String failureReason) {
+ mActivity.onPurchaseFailed(failureCode, failureReason);
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 0988cba..01348e4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -23,6 +23,8 @@
import android.credentials.CreateCredentialRequest
import android.credentials.ui.Constants
import android.credentials.ui.Entry
+import android.credentials.ui.CreateCredentialProviderData
+import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.ProviderData
import android.credentials.ui.RequestInfo
import android.credentials.ui.BaseDialogResult
@@ -54,10 +56,22 @@
RequestInfo::class.java
) ?: testRequestInfo()
- providerList = intent.extras?.getParcelableArrayList(
- ProviderData.EXTRA_PROVIDER_DATA_LIST,
- ProviderData::class.java
- ) ?: testProviderList()
+ providerList = when (requestInfo.type) {
+ RequestInfo.TYPE_CREATE ->
+ intent.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ CreateCredentialProviderData::class.java
+ ) ?: testCreateCredentialProviderList()
+ RequestInfo.TYPE_GET ->
+ intent.extras?.getParcelableArrayList(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ GetCredentialProviderData::class.java
+ ) ?: testGetCredentialProviderList()
+ else -> {
+ // TODO: fail gracefully
+ throw IllegalStateException("Unrecognized request type: ${requestInfo.type}")
+ }
+ }
resultReceiver = intent.getParcelableExtra(
Constants.EXTRA_RESULT_RECEIVER,
@@ -84,7 +98,9 @@
}
fun getCredentialInitialUiState(): GetCredentialUiState {
- val providerList = GetFlowUtils.toProviderList(providerList, context)
+ val providerList = GetFlowUtils.toProviderList(
+ // TODO: handle runtime cast error
+ providerList as List<GetCredentialProviderData>, context)
// TODO: covert from real requestInfo
val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo(
"Elisa Beckett",
@@ -100,7 +116,9 @@
}
fun createPasskeyInitialUiState(): CreatePasskeyUiState {
- val providerList = CreateFlowUtils.toProviderList(providerList, context)
+ val providerList = CreateFlowUtils.toProviderList(
+ // Handle runtime cast error
+ providerList as List<CreateCredentialProviderData>, context)
// TODO: covert from real requestInfo
val requestDisplayInfo = RequestDisplayInfo(
"Elisa Beckett",
@@ -130,32 +148,29 @@
}
// TODO: below are prototype functionalities. To be removed for productionization.
- private fun testProviderList(): List<ProviderData> {
+ private fun testCreateCredentialProviderList(): List<CreateCredentialProviderData> {
return listOf(
- ProviderData.Builder(
- "com.google",
- "Google Password Manager",
- Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
- .setCredentialEntries(
+ CreateCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
+ .setSaveEntries(
listOf<Entry>(
newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
"Elisa Backett", "20 passwords and 7 passkeys saved"),
newEntry("key1", "subkey-2", "elisa.work@google.com",
"Elisa Backett Work", "20 passwords and 7 passkeys saved"),
)
- ).setActionChips(
+ )
+ .setActionChips(
listOf<Entry>(
newEntry("key2", "subkey-1", "Go to Settings", "",
"20 passwords and 7 passkeys saved"),
newEntry("key2", "subkey-2", "Switch Account", "",
"20 passwords and 7 passkeys saved"),
),
- ).build(),
- ProviderData.Builder(
- "com.dashlane",
- "Dashlane",
- Icon.createWithResource(context, R.drawable.ic_launcher_foreground))
- .setCredentialEntries(
+ )
+ .setIsDefaultProvider(true)
+ .build(),
+ CreateCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService")
+ .setSaveEntries(
listOf<Entry>(
newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
"Elisa Backett", "20 passwords and 7 passkeys saved"),
@@ -172,6 +187,42 @@
)
}
+ private fun testGetCredentialProviderList(): List<GetCredentialProviderData> {
+ return listOf(
+ GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
+ .setCredentialEntries(
+ listOf<Entry>(
+ newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
+ "Elisa Backett", "20 passwords and 7 passkeys saved"),
+ newEntry("key1", "subkey-2", "elisa.work@google.com",
+ "Elisa Backett Work", "20 passwords and 7 passkeys saved"),
+ )
+ ).setActionChips(
+ listOf<Entry>(
+ newEntry("key2", "subkey-1", "Go to Settings", "",
+ "20 passwords and 7 passkeys saved"),
+ newEntry("key2", "subkey-2", "Switch Account", "",
+ "20 passwords and 7 passkeys saved"),
+ ),
+ ).build(),
+ GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService")
+ .setCredentialEntries(
+ listOf<Entry>(
+ newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
+ "Elisa Backett", "20 passwords and 7 passkeys saved"),
+ newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
+ "Elisa Backett Work", "20 passwords and 7 passkeys saved"),
+ )
+ ).setActionChips(
+ listOf<Entry>(
+ newEntry("key2", "subkey-3", "Manage Accounts",
+ "Manage your accounts in the dashlane app",
+ "20 passwords and 7 passkeys saved"),
+ ),
+ ).build(),
+ )
+ }
+
private fun newEntry(
key: String,
subkey: String,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 2ba8748..bf0dba2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -18,7 +18,8 @@
import android.content.Context
import android.credentials.ui.Entry
-import android.credentials.ui.ProviderData
+import android.credentials.ui.GetCredentialProviderData
+import android.credentials.ui.CreateCredentialProviderData
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.getflow.CredentialOptionInfo
import com.android.credentialmanager.getflow.ProviderInfo
@@ -28,7 +29,7 @@
companion object {
fun toProviderList(
- providerDataList: List<ProviderData>,
+ providerDataList: List<GetCredentialProviderData>,
context: Context,
): List<ProviderInfo> {
return providerDataList.map {
@@ -36,9 +37,10 @@
// TODO: replace to extract from the service data structure when available
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = it.providerFlattenedComponentName,
- displayName = it.providerDisplayName,
+ // TODO: get the service display name and icon from the component name.
+ displayName = it.providerFlattenedComponentName,
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context)
+ credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context),
)
}
}
@@ -72,7 +74,7 @@
companion object {
fun toProviderList(
- providerDataList: List<ProviderData>,
+ providerDataList: List<CreateCredentialProviderData>,
context: Context,
): List<com.android.credentialmanager.createflow.ProviderInfo> {
return providerDataList.map {
@@ -80,9 +82,11 @@
// TODO: replace to extract from the service data structure when available
icon = context.getDrawable(R.drawable.ic_passkey)!!,
name = it.providerFlattenedComponentName,
- displayName = it.providerDisplayName,
+ // TODO: get the service display name and icon from the component name.
+ displayName = it.providerFlattenedComponentName,
credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
- createOptions = toCreationOptionInfoList(it.credentialEntries, context),
+ createOptions = toCreationOptionInfoList(it.saveEntries, context),
+ isDefault = it.isDefaultProvider,
)
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index cb2bf10..db0f337e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -24,6 +24,7 @@
val displayName: String,
val credentialTypeIcon: Drawable,
val createOptions: List<CreateOptionInfo>,
+ val isDefault: Boolean,
)
data class CreateOptionInfo(
diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml
index e30d441..8d44315 100644
--- a/packages/SystemUI/res/values/integers.xml
+++ b/packages/SystemUI/res/values/integers.xml
@@ -35,4 +35,6 @@
<!-- Percentage of displacement for items in QQS to guarantee matching with bottom of clock at
fade_out_complete_frame -->
<dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen>
+
+ <integer name="qs_carrier_max_em">7</integer>
</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index bffde32..79a01b9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -353,10 +353,21 @@
if (!mSidefpsController.isPresent()) {
return;
}
- if (mBouncerVisible
- && getResources().getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
- && mUpdateMonitor.isFingerprintDetectionRunning()
- && !mUpdateMonitor.userNeedsStrongAuth()) {
+ final boolean sfpsEnabled = getResources().getBoolean(
+ R.bool.config_show_sidefps_hint_on_bouncer);
+ final boolean fpsDetectionRunning = mUpdateMonitor.isFingerprintDetectionRunning();
+ final boolean needsStrongAuth = mUpdateMonitor.userNeedsStrongAuth();
+
+ boolean toShow = mBouncerVisible && sfpsEnabled && fpsDetectionRunning && !needsStrongAuth;
+
+ if (DEBUG) {
+ Log.d(TAG, "sideFpsToShow=" + toShow + ", "
+ + "mBouncerVisible=" + mBouncerVisible + ", "
+ + "configEnabled=" + sfpsEnabled + ", "
+ + "fpsDetectionRunning=" + fpsDetectionRunning + ", "
+ + "needsStrongAuth=" + needsStrongAuth);
+ }
+ if (toShow) {
mSidefpsController.get().show();
} else {
mSidefpsController.get().hide();
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
index 22dc94a..5850c95 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/UserBroadcastDispatcher.kt
@@ -21,6 +21,7 @@
import android.content.Context
import android.os.Handler
import android.os.Looper
+import android.os.Trace
import android.os.UserHandle
import android.util.ArrayMap
import android.util.ArraySet
@@ -126,6 +127,7 @@
action,
userId,
{
+ Trace.beginSection("registerReceiver act=$action user=$userId")
context.registerReceiverAsUser(
this,
UserHandle.of(userId),
@@ -134,11 +136,14 @@
workerHandler,
flags
)
+ Trace.endSection()
logger.logContextReceiverRegistered(userId, flags, it)
},
{
try {
+ Trace.beginSection("unregisterReceiver act=$action user=$userId")
context.unregisterReceiver(this)
+ Trace.endSection()
logger.logContextReceiverUnregistered(userId, action)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Trying to unregister unregistered receiver for user $userId, " +
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 48bef97..2bee75e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -41,6 +41,7 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
+import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeControllerImpl;
@@ -93,6 +94,7 @@
AospPolicyModule.class,
GestureModule.class,
MediaModule.class,
+ MultiUserUtilsModule.class,
PowerModule.class,
QSModule.class,
ReferenceScreenshotModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 6db56210..482bdaf 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -58,7 +58,6 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.security.data.repository.SecurityRepositoryModule;
-import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.smartspace.dagger.SmartspaceModule;
import com.android.systemui.statusbar.CommandQueue;
@@ -140,7 +139,6 @@
PrivacyModule.class,
ScreenshotModule.class,
SensorModule.class,
- MultiUserUtilsModule.class,
SecurityRepositoryModule.class,
SettingsUtilModule.class,
SmartRepliesInflationModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 7b1ba0d..8bcf9f6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -239,11 +239,13 @@
@JvmField val ROUNDED_BOX_RIPPLE = ReleasedFlag(1002)
// 1100 - windowing
+ @JvmField
@Keep
val WM_ENABLE_SHELL_TRANSITIONS =
SysPropBooleanFlag(1100, "persist.wm.debug.shell_transit", false)
/** b/170163464: animate bubbles expanded view collapse with home gesture */
+ @JvmField
@Keep
val BUBBLES_HOME_GESTURE =
SysPropBooleanFlag(1101, "persist.wm.debug.bubbles_home_gesture", true)
@@ -265,40 +267,50 @@
@Keep
val HIDE_NAVBAR_WINDOW = SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false)
+ @JvmField
@Keep
val WM_DESKTOP_WINDOWING = SysPropBooleanFlag(1104, "persist.wm.debug.desktop_mode", false)
+ @JvmField
@Keep
val WM_CAPTION_ON_SHELL = SysPropBooleanFlag(1105, "persist.wm.debug.caption_on_shell", false)
+ @JvmField
@Keep
val FLOATING_TASKS_ENABLED = SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false)
+ @JvmField
@Keep
val SHOW_FLOATING_TASKS_AS_BUBBLES =
SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false)
+ @JvmField
@Keep
val ENABLE_FLING_TO_DISMISS_BUBBLE =
SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true)
+ @JvmField
@Keep
val ENABLE_FLING_TO_DISMISS_PIP =
SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true)
+ @JvmField
@Keep
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
SysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", false)
// 1200 - predictive back
+ @JvmField
@Keep
val WM_ENABLE_PREDICTIVE_BACK =
SysPropBooleanFlag(1200, "persist.wm.debug.predictive_back", true)
+ @JvmField
@Keep
val WM_ENABLE_PREDICTIVE_BACK_ANIM =
SysPropBooleanFlag(1201, "persist.wm.debug.predictive_back_anim", false)
+ @JvmField
@Keep
val WM_ALWAYS_ENFORCE_PREDICTIVE_BACK =
SysPropBooleanFlag(1202, "persist.wm.debug.predictive_back_always_enforce", false)
@@ -308,7 +320,7 @@
// 1300 - screenshots
// TODO(b/254512719): Tracking Bug
- @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300)
+ @JvmField val SCREENSHOT_REQUEST_PROCESSOR = UnreleasedFlag(1300, true)
// TODO(b/254513155): Tracking Bug
@JvmField val SCREENSHOT_WORK_PROFILE_POLICY = UnreleasedFlag(1301)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
index 703b95a..b5ceeae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrier.java
@@ -19,6 +19,7 @@
import android.annotation.StyleRes;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -33,6 +34,7 @@
import com.android.settingslib.graph.SignalDrawable;
import com.android.systemui.FontSizeUtils;
import com.android.systemui.R;
+import com.android.systemui.util.LargeScreenUtils;
import java.util.Objects;
@@ -72,6 +74,7 @@
mMobileSignal = findViewById(R.id.mobile_signal);
mCarrierText = findViewById(R.id.qs_carrier_text);
mSpacer = findViewById(R.id.spacer);
+ updateResources();
}
/**
@@ -142,4 +145,20 @@
public void updateTextAppearance(@StyleRes int resId) {
FontSizeUtils.updateFontSizeFromStyle(mCarrierText, resId);
}
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateResources();
+ }
+
+ private void updateResources() {
+ boolean useLargeScreenHeader =
+ LargeScreenUtils.shouldUseLargeScreenShadeHeader(getResources());
+ mCarrierText.setMaxEms(
+ useLargeScreenHeader
+ ? Integer.MAX_VALUE
+ : getResources().getInteger(R.integer.qs_carrier_max_em)
+ );
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 6711734..cd5647e 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -30,7 +30,6 @@
import androidx.annotation.WorkerThread
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
-import com.android.systemui.people.widget.PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE
import com.android.systemui.util.Assert
import java.io.PrintWriter
import java.lang.ref.WeakReference
@@ -53,7 +52,7 @@
*
* Class constructed and initialized in [SettingsModule].
*/
-class UserTrackerImpl internal constructor(
+open class UserTrackerImpl internal constructor(
private val context: Context,
private val userManager: UserManager,
private val dumpManager: DumpManager,
@@ -70,13 +69,13 @@
private val mutex = Any()
override var userId: Int by SynchronizedDelegate(context.userId)
- private set
+ protected set
override var userHandle: UserHandle by SynchronizedDelegate(context.user)
- private set
+ protected set
override var userContext: Context by SynchronizedDelegate(context)
- private set
+ protected set
override val userContentResolver: ContentResolver
get() = userContext.contentResolver
@@ -94,7 +93,7 @@
* modified.
*/
override var userProfiles: List<UserInfo> by SynchronizedDelegate(emptyList())
- private set
+ protected set
@GuardedBy("callbacks")
private val callbacks: MutableList<DataItem> = ArrayList()
@@ -155,7 +154,7 @@
}
@WorkerThread
- private fun handleSwitchUser(newUser: Int) {
+ protected open fun handleSwitchUser(newUser: Int) {
Assert.isNotMainThread()
if (newUser == UserHandle.USER_NULL) {
Log.w(TAG, "handleSwitchUser - Couldn't get new id from intent")
@@ -174,7 +173,7 @@
}
@WorkerThread
- private fun handleProfilesChanged() {
+ protected open fun handleProfilesChanged() {
Assert.isNotMainThread()
val profiles = userManager.getProfiles(userId)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8a31ed9..470cbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -198,6 +198,13 @@
// At this point we just need to initiate the transfer
val summaryUpdate = mPostedEntries[logicalSummary.key]
+ // Because we now know for certain that some child is going to alert for this summary
+ // (as we have found a child to transfer the alert to), mark the group as having
+ // interrupted. This will allow us to know in the future that the "should heads up"
+ // state of this group has already been handled, just not via the summary entry itself.
+ logicalSummary.setInterruption()
+ mLogger.logSummaryMarkedInterrupted(logicalSummary.key, childToReceiveParentAlert.key)
+
// If the summary was not attached, then remove the alert from the detached summary.
// Otherwise we can simply ignore its posted update.
if (!isSummaryAttached) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index dfaa291..473c35d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -69,4 +69,13 @@
"updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
})
}
+
+ fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = summaryKey
+ str2 = childKey
+ }, {
+ "marked group summary as interrupted: $str1 for alert transfer to child: $str2"
+ })
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
new file mode 100644
index 0000000..e618905
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.net.NetworkCapabilities
+
+/** Provides information about a mobile network connection */
+data class MobileConnectivityModel(
+ /** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */
+ val isConnected: Boolean = false,
+ /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
+ val isValidated: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 06e8f46..581842b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -16,11 +16,15 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import android.content.Context
+import android.database.ContentObserver
+import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
@@ -34,6 +38,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.util.settings.GlobalSettings
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -42,9 +47,12 @@
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/**
@@ -65,14 +73,23 @@
*/
val subscriptionModelFlow: Flow<MobileSubscriptionModel>
/** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
- val dataEnabled: Flow<Boolean>
+ val dataEnabled: StateFlow<Boolean>
+ /**
+ * True if this connection represents the default subscription per
+ * [SubscriptionManager.getDefaultDataSubscriptionId]
+ */
+ val isDefaultDataSubscription: StateFlow<Boolean>
}
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
+ private val context: Context,
private val subId: Int,
private val telephonyManager: TelephonyManager,
+ private val globalSettings: GlobalSettings,
+ defaultDataSubId: StateFlow<Int>,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
bgDispatcher: CoroutineDispatcher,
logger: ConnectivityPipelineLogger,
scope: CoroutineScope,
@@ -86,6 +103,8 @@
}
}
+ private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
var state = MobileSubscriptionModel()
conflatedCallbackFlow {
@@ -165,33 +184,75 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
+ .onEach { telephonyCallbackEvent.tryEmit(Unit) }
.logOutputChange(logger, "MobileSubscriptionModel")
.stateIn(scope, SharingStarted.WhileSubscribed(), state)
}
+ /** Produces whenever the mobile data setting changes for this subId */
+ private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
+ /* notifyForDescendants */ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+ }
+
/**
* There are a few cases where we will need to poll [TelephonyManager] so we can update some
* internal state where callbacks aren't provided. Any of those events should be merged into
* this flow, which can be used to trigger the polling.
*/
- private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {}
+ private val telephonyPollingEvent: Flow<Unit> =
+ merge(
+ telephonyCallbackEvent,
+ localMobileDataSettingChangedEvent,
+ globalMobileDataSettingChangedEvent,
+ )
- override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() }
+ override val dataEnabled: StateFlow<Boolean> =
+ telephonyPollingEvent
+ .mapLatest { dataConnectionAllowed() }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())
private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
+ override val isDefaultDataSubscription: StateFlow<Boolean> =
+ defaultDataSubId
+ .mapLatest { it == subId }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId)
+
class Factory
@Inject
constructor(
+ private val context: Context,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
+ private val globalSettings: GlobalSettings,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
) {
- fun build(subId: Int): MobileConnectionRepository {
+ fun build(
+ subId: Int,
+ defaultDataSubId: StateFlow<Int>,
+ globalMobileDataSettingChangedEvent: Flow<Unit>,
+ ): MobileConnectionRepository {
return MobileConnectionRepositoryImpl(
+ context,
subId,
telephonyManager.createForSubscriptionId(subId),
+ globalSettings,
+ defaultDataSubId,
+ globalMobileDataSettingChangedEvent,
bgDispatcher,
logger,
scope,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index 0e2428a..c3c1f14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -16,15 +16,27 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import android.annotation.SuppressLint
import android.content.Context
import android.content.IntentFilter
+import android.database.ContentObserver
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings
+import android.provider.Settings.Global.MOBILE_DATA
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
+import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -32,7 +44,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -40,10 +54,12 @@
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -57,13 +73,22 @@
val subscriptionsFlow: Flow<List<SubscriptionInfo>>
/** Observable for the subscriptionId of the current mobile data connection */
- val activeMobileDataSubscriptionId: Flow<Int>
+ val activeMobileDataSubscriptionId: StateFlow<Int>
/** Observable for [MobileMappings.Config] tracking the defaults */
val defaultDataSubRatConfig: StateFlow<Config>
+ /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
+ val defaultDataSubId: StateFlow<Int>
+
+ /** The current connectivity status for the default mobile network connection */
+ val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
+
/** Get or create a repository for the line of service for the given subscription ID */
fun getRepoForSubId(subId: Int): MobileConnectionRepository
+
+ /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
+ val globalMobileDataSettingChangedEvent: Flow<Unit>
}
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@@ -72,10 +97,12 @@
class MobileConnectionsRepositoryImpl
@Inject
constructor(
+ private val connectivityManager: ConnectivityManager,
private val subscriptionManager: SubscriptionManager,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
broadcastDispatcher: BroadcastDispatcher,
+ private val globalSettings: GlobalSettings,
private val context: Context,
@Background private val bgDispatcher: CoroutineDispatcher,
@Application private val scope: CoroutineScope,
@@ -121,17 +148,26 @@
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
+
+ private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
+ MutableSharedFlow(extraBufferCapacity = 1)
+
+ override val defaultDataSubId: StateFlow<Int> =
+ broadcastDispatcher
+ .broadcastFlow(
+ IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ ) { intent, _ ->
+ intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+ }
+ .distinctUntilChanged()
+ .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
- started = SharingStarted.WhileSubscribed(),
- SubscriptionManager.INVALID_SUBSCRIPTION_ID
+ SharingStarted.WhileSubscribed(),
+ SubscriptionManager.getDefaultDataSubscriptionId()
)
- private val defaultDataSubChangedEvent =
- broadcastDispatcher.broadcastFlow(
- IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- )
-
private val carrierConfigChangedEvent =
broadcastDispatcher.broadcastFlow(
IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
@@ -148,9 +184,8 @@
* This flow will produce whenever the default data subscription or the carrier config changes.
*/
override val defaultDataSubRatConfig: StateFlow<Config> =
- combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
- Config.readConfig(context)
- }
+ merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
+ .mapLatest { Config.readConfig(context) }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
@@ -168,6 +203,57 @@
?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
}
+ /**
+ * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
+ * connection repositories also observe the URI for [MOBILE_DATA] + subId.
+ */
+ override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ globalSettings.registerContentObserver(
+ globalSettings.getUriFor(MOBILE_DATA),
+ true,
+ observer
+ )
+
+ awaitClose { context.contentResolver.unregisterContentObserver(observer) }
+ }
+
+ @SuppressLint("MissingPermission")
+ override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
+ conflatedCallbackFlow {
+ val callback =
+ object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+ override fun onLost(network: Network) {
+ // Send a disconnected model when lost. Maybe should create a sealed
+ // type or null here?
+ trySend(MobileConnectivityModel())
+ }
+
+ override fun onCapabilitiesChanged(
+ network: Network,
+ caps: NetworkCapabilities
+ ) {
+ trySend(
+ MobileConnectivityModel(
+ isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
+ isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
+ )
+ )
+ }
+ }
+
+ connectivityManager.registerDefaultNetworkCallback(callback)
+
+ awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
+
private fun isValidSubId(subId: Int): Boolean {
subscriptionsFlow.value.forEach {
if (it.subscriptionId == subId) {
@@ -181,7 +267,11 @@
@VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
- return mobileConnectionRepositoryFactory.build(subId)
+ return mobileConnectionRepositoryFactory.build(
+ subId,
+ defaultDataSubId,
+ globalMobileDataSettingChangedEvent,
+ )
}
private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
index 77de849..91886bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt
@@ -26,7 +26,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
@@ -40,7 +39,7 @@
*/
interface UserSetupRepository {
/** Observable tracking [DeviceProvisionedController.isUserSetup] */
- val isUserSetupFlow: Flow<Boolean>
+ val isUserSetupFlow: StateFlow<Boolean>
}
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index f99d278c..0da84f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -18,81 +18,109 @@
import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
interface MobileIconInteractor {
+ /** Only true if mobile is the default transport but is not validated, otherwise false */
+ val isDefaultConnectionFailed: StateFlow<Boolean>
+
+ /** True when telephony tells us that the data state is CONNECTED */
+ val isDataConnected: StateFlow<Boolean>
+
+ // TODO(b/256839546): clarify naming of default vs active
+ /** True if we want to consider the data connection enabled */
+ val isDefaultDataEnabled: StateFlow<Boolean>
+
/** Observable for the data enabled state of this connection */
- val isDataEnabled: Flow<Boolean>
+ val isDataEnabled: StateFlow<Boolean>
/** Observable for RAT type (network type) indicator */
- val networkTypeIconGroup: Flow<MobileIconGroup>
+ val networkTypeIconGroup: StateFlow<MobileIconGroup>
/** True if this line of service is emergency-only */
- val isEmergencyOnly: Flow<Boolean>
+ val isEmergencyOnly: StateFlow<Boolean>
/** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
- val level: Flow<Int>
+ val level: StateFlow<Int>
/** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
- val numberOfLevels: Flow<Int>
-
- /** True when we want to draw an icon that makes room for the exclamation mark */
- val cutOut: Flow<Boolean>
+ val numberOfLevels: StateFlow<Int>
}
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconInteractorImpl(
- defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
- defaultMobileIconGroup: Flow<MobileIconGroup>,
+ @Application scope: CoroutineScope,
+ defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
+ defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
+ defaultMobileIconGroup: StateFlow<MobileIconGroup>,
+ override val isDefaultConnectionFailed: StateFlow<Boolean>,
mobileMappingsProxy: MobileMappingsProxy,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
- override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled
+ override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
+
+ override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
/** Observable for the current RAT indicator icon ([MobileIconGroup]) */
- override val networkTypeIconGroup: Flow<MobileIconGroup> =
+ override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
combine(
- mobileStatusInfo,
- defaultMobileIconMapping,
- defaultMobileIconGroup,
- ) { info, mapping, defaultGroup ->
- val lookupKey =
- when (val resolved = info.resolvedNetworkType) {
- is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
- is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
- }
- mapping[lookupKey] ?: defaultGroup
- }
-
- override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
-
- override val level: Flow<Int> =
- mobileStatusInfo.map { mobileModel ->
- // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
- if (mobileModel.isGsm) {
- mobileModel.primaryLevel
- } else {
- mobileModel.cdmaLevel
+ mobileStatusInfo,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ ) { info, mapping, defaultGroup ->
+ val lookupKey =
+ when (val resolved = info.resolvedNetworkType) {
+ is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
+ is OverrideNetworkType ->
+ mobileMappingsProxy.toIconKeyOverride(resolved.type)
+ }
+ mapping[lookupKey] ?: defaultGroup
}
- }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
+
+ override val isEmergencyOnly: StateFlow<Boolean> =
+ mobileStatusInfo
+ .mapLatest { it.isEmergencyOnly }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val level: StateFlow<Int> =
+ mobileStatusInfo
+ .mapLatest { mobileModel ->
+ // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
+ if (mobileModel.isGsm) {
+ mobileModel.primaryLevel
+ } else {
+ mobileModel.cdmaLevel
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
/**
* This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
* once it's wired up inside of [CarrierConfigTracker]
*/
- override val numberOfLevels: Flow<Int> = flowOf(4)
+ override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4)
- /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
- // TODO: find a better name for this?
- override val cutOut: Flow<Boolean> = flowOf(false)
+ override val isDataConnected: StateFlow<Boolean> =
+ mobileStatusInfo
+ .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 614d583..a4175c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -19,6 +19,7 @@
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
@@ -35,7 +36,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
/**
@@ -51,12 +54,16 @@
interface MobileIconsInteractor {
/** List of subscriptions, potentially filtered for CBRS */
val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+ /** True if the active mobile data subscription has data enabled */
+ val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
- val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+ val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
- val defaultMobileIconGroup: Flow<MobileIconGroup>
+ val defaultMobileIconGroup: StateFlow<MobileIconGroup>
+ /** True only if the default network is mobile, and validation also failed */
+ val isDefaultConnectionFailed: StateFlow<Boolean>
/** True once the user has been set up */
- val isUserSetup: Flow<Boolean>
+ val isUserSetup: StateFlow<Boolean>
/**
* Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
* subId. Will throw if the ID is invalid
@@ -79,6 +86,22 @@
private val activeMobileDataSubscriptionId =
mobileConnectionsRepo.activeMobileDataSubscriptionId
+ private val activeMobileDataConnectionRepo: StateFlow<MobileConnectionRepository?> =
+ activeMobileDataSubscriptionId
+ .mapLatest { activeId ->
+ if (activeId == INVALID_SUBSCRIPTION_ID) {
+ null
+ } else {
+ mobileConnectionsRepo.getRepoForSubId(activeId)
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
+ activeMobileDataConnectionRepo
+ .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
mobileConnectionsRepo.subscriptionsFlow
@@ -132,22 +155,40 @@
*/
override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
mobileConnectionsRepo.defaultDataSubRatConfig
- .map { mobileMappingsProxy.mapIconSets(it) }
+ .mapLatest { mobileMappingsProxy.mapIconSets(it) }
.stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
/** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
mobileConnectionsRepo.defaultDataSubRatConfig
- .map { mobileMappingsProxy.getDefaultIcons(it) }
+ .mapLatest { mobileMappingsProxy.getDefaultIcons(it) }
.stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
- override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+ /**
+ * We want to show an error state when cellular has actually failed to validate, but not if some
+ * other transport type is active, because then we expect there not to be validation.
+ */
+ override val isDefaultConnectionFailed: StateFlow<Boolean> =
+ mobileConnectionsRepo.defaultMobileNetworkConnectivity
+ .mapLatest { connectivityModel ->
+ if (!connectivityModel.isConnected) {
+ false
+ } else {
+ !connectivityModel.isValidated
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
/** Vends out new [MobileIconInteractor] for a particular subId */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
MobileIconInteractorImpl(
+ scope,
+ activeDataConnectionHasDataEnabled,
defaultMobileIconMapping,
defaultMobileIconGroup,
+ isDefaultConnectionFailed,
mobileMappingsProxy,
mobileConnectionsRepo.getRepoForSubId(subId),
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 8131739..7869021 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -24,10 +24,12 @@
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.mapLatest
/**
* View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -39,29 +41,38 @@
*
* TODO: figure out where carrier merged and VCN models go (probably here?)
*/
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel
constructor(
val subscriptionId: Int,
iconInteractor: MobileIconInteractor,
logger: ConnectivityPipelineLogger,
) {
+ /** Whether or not to show the error state of [SignalDrawable] */
+ private val showExclamationMark: Flow<Boolean> =
+ iconInteractor.isDefaultDataEnabled.mapLatest { !it }
+
/** An int consumable by [SignalDrawable] for display */
- var iconId: Flow<Int> =
- combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
+ val iconId: Flow<Int> =
+ combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
level,
numberOfLevels,
- cutOut ->
- SignalDrawable.getState(level, numberOfLevels, cutOut)
+ showExclamationMark ->
+ SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
}
.distinctUntilChanged()
.logOutputChange(logger, "iconId($subscriptionId)")
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
- var networkTypeIcon: Flow<Icon?> =
- combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) {
- networkTypeIconGroup,
- isDataEnabled ->
- if (!isDataEnabled) {
+ val networkTypeIcon: Flow<Icon?> =
+ combine(
+ iconInteractor.networkTypeIconGroup,
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.isDefaultConnectionFailed,
+ ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection ->
+ if (!dataConnected || !dataEnabled || failedConnection) {
null
} else {
val desc =
@@ -72,5 +83,5 @@
}
}
- var tint: Flow<Int> = flowOf(Color.CYAN)
+ val tint: Flow<Int> = flowOf(Color.CYAN)
}
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 10a09dd1..61eadeb 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -44,6 +44,7 @@
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
+import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeControllerImpl;
@@ -89,6 +90,7 @@
includes = {
AospPolicyModule.class,
GestureModule.class,
+ MultiUserUtilsModule.class,
PowerModule.class,
QSModule.class,
ReferenceScreenshotModule.class,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
index 99a17a6..9115ab3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierTest.java
@@ -24,6 +24,7 @@
import android.testing.TestableLooper;
import android.view.LayoutInflater;
import android.view.View;
+import android.widget.TextView;
import androidx.test.filters.SmallTest;
@@ -48,6 +49,7 @@
public void setUp() throws Exception {
mTestableLooper = TestableLooper.get(this);
LayoutInflater inflater = LayoutInflater.from(mContext);
+ mContext.ensureTestableResources();
mTestableLooper.runWithLooper(() ->
mQSCarrier = (QSCarrier) inflater.inflate(R.layout.qs_carrier, null));
@@ -119,4 +121,30 @@
mQSCarrier.updateState(c, true);
assertEquals(View.GONE, mQSCarrier.getRSSIView().getVisibility());
}
+
+ @Test
+ public void testCarrierNameMaxWidth_smallScreen_fromResource() {
+ int maxEms = 10;
+ mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms);
+ mContext.getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_large_screen_shade_header, false);
+ TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text);
+
+ mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration());
+
+ assertEquals(maxEms, carrierText.getMaxEms());
+ }
+
+ @Test
+ public void testCarrierNameMaxWidth_largeScreen_maxInt() {
+ int maxEms = 10;
+ mContext.getOrCreateTestableResources().addOverride(R.integer.qs_carrier_max_em, maxEms);
+ mContext.getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_large_screen_shade_header, true);
+ TextView carrierText = mQSCarrier.requireViewById(R.id.qs_carrier_text);
+
+ mQSCarrier.onConfigurationChanged(mContext.getResources().getConfiguration());
+
+ assertEquals(Integer.MAX_VALUE, carrierText.getMaxEms());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 3ff7639..f96c39f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -406,6 +406,10 @@
verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
verify(mHeadsUpManager).showNotification(mGroupSibling1)
+
+ // In addition make sure we have explicitly marked the summary as having interrupted due
+ // to the alert being transferred
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -424,6 +428,7 @@
verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
verify(mHeadsUpManager).showNotification(mGroupChild1)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -449,6 +454,7 @@
verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
verify(mHeadsUpManager).showNotification(mGroupSibling1)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -474,6 +480,7 @@
verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
verify(mHeadsUpManager).showNotification(mGroupChild1)
verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -512,6 +519,7 @@
verify(mHeadsUpManager).showNotification(mGroupPriority)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -548,6 +556,7 @@
verify(mHeadsUpManager).showNotification(mGroupPriority)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -582,6 +591,7 @@
verify(mHeadsUpManager).showNotification(mGroupPriority)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling1)
verify(mHeadsUpManager, never()).showNotification(mGroupSibling2)
+ assertTrue(mGroupSummary.hasInterrupted())
}
@Test
@@ -672,6 +682,35 @@
}
@Test
+ fun testNoTransfer_groupSummaryNotAlerting() {
+ // When we have a group where the summary should not alert and exactly one child should
+ // alert, we should never mark the group summary as interrupted (because it doesn't).
+ setShouldHeadsUp(mGroupSummary, false)
+ setShouldHeadsUp(mGroupChild1, true)
+ setShouldHeadsUp(mGroupChild2, false)
+
+ mCollectionListener.onEntryAdded(mGroupSummary)
+ mCollectionListener.onEntryAdded(mGroupChild1)
+ mCollectionListener.onEntryAdded(mGroupChild2)
+ val groupEntry = GroupEntryBuilder()
+ .setSummary(mGroupSummary)
+ .setChildren(listOf(mGroupChild1, mGroupChild2))
+ .build()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(groupEntry))
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(groupEntry))
+
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupSummary), any())
+ finishBind(mGroupChild1)
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(eq(mGroupChild2), any())
+
+ verify(mHeadsUpManager, never()).showNotification(mGroupSummary)
+ verify(mHeadsUpManager).showNotification(mGroupChild1)
+ verify(mHeadsUpManager, never()).showNotification(mGroupChild2)
+ assertFalse(mGroupSummary.hasInterrupted())
+ }
+
+ @Test
fun testOnRankingApplied_newEntryShouldAlert() {
// GIVEN that mEntry has never interrupted in the past, and now should
// and is new enough to do so
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index de1fec8..288f54c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -17,16 +17,18 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeMobileConnectionRepository : MobileConnectionRepository {
private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
- override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
+ override val subscriptionModelFlow = _subscriptionsModelFlow
private val _dataEnabled = MutableStateFlow(true)
override val dataEnabled = _dataEnabled
+ private val _isDefaultDataSubscription = MutableStateFlow(true)
+ override val isDefaultDataSubscription = _isDefaultDataSubscription
+
fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
_subscriptionsModelFlow.value = model
}
@@ -34,4 +36,8 @@
fun setDataEnabled(enabled: Boolean) {
_dataEnabled.value = enabled
}
+
+ fun setIsDefaultDataSubscription(isDefault: Boolean) {
+ _isDefaultDataSubscription.value = isDefault
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 813e750..533d5d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -17,8 +17,9 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,18 +27,26 @@
private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
- private val _activeMobileDataSubscriptionId =
- MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+ private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
private val _defaultDataSubRatConfig = MutableStateFlow(Config())
override val defaultDataSubRatConfig = _defaultDataSubRatConfig
+ private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
+ override val defaultDataSubId = _defaultDataSubId
+
+ private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
+ override val defaultMobileNetworkConnectivity = _mobileConnectivity
+
private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
}
+ private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
+ override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
+
fun setSubscriptions(subs: List<SubscriptionInfo>) {
_subscriptionsFlow.value = subs
}
@@ -46,6 +55,18 @@
_defaultDataSubRatConfig.value = config
}
+ fun setDefaultDataSubId(id: Int) {
+ _defaultDataSubId.value = id
+ }
+
+ fun setMobileConnectivity(model: MobileConnectivityModel) {
+ _mobileConnectivity.value = model
+ }
+
+ suspend fun triggerGlobalMobileDataSettingChangedEvent() {
+ _globalMobileDataSettingChangedEvent.emit(Unit)
+ }
+
fun setActiveMobileDataSubscriptionId(subId: Int) {
_activeMobileDataSubscriptionId.value = subId
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
index 6c495c5..141b50c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt
@@ -16,13 +16,12 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/** Defaults to `true` */
class FakeUserSetupRepository : UserSetupRepository {
private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true)
- override val isUserSetupFlow: Flow<Boolean> = _isUserSetup
+ override val isUserSetupFlow = _isUserSetup
fun setUserSetup(setup: Boolean) {
_isUserSetup.value = setup
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
index 0939364..5ce51bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import android.os.UserHandle
+import android.provider.Settings
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
@@ -42,6 +44,7 @@
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -67,16 +70,23 @@
@Mock private lateinit var logger: ConnectivityPipelineLogger
private val scope = CoroutineScope(IMMEDIATE)
+ private val globalSettings = FakeSettings()
+ private val connectionsRepo = FakeMobileConnectionsRepository()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ globalSettings.userId = UserHandle.USER_ALL
whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
underTest =
MobileConnectionRepositoryImpl(
+ context,
SUB_1_ID,
telephonyManager,
+ globalSettings,
+ connectionsRepo.defaultDataSubId,
+ connectionsRepo.globalMobileDataSettingChangedEvent,
IMMEDIATE,
logger,
scope,
@@ -290,14 +300,20 @@
}
@Test
- fun dataEnabled_isEnabled() =
+ fun dataEnabled_initial_false() =
runBlocking(IMMEDIATE) {
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
- var latest: Boolean? = null
- val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+ assertThat(underTest.dataEnabled.value).isFalse()
+ }
- assertThat(latest).isTrue()
+ @Test
+ fun dataEnabled_isEnabled_true() =
+ runBlocking(IMMEDIATE) {
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+ val job = underTest.dataEnabled.launchIn(this)
+
+ assertThat(underTest.dataEnabled.value).isTrue()
job.cancel()
}
@@ -306,10 +322,59 @@
fun dataEnabled_isDisabled() =
runBlocking(IMMEDIATE) {
whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+ val job = underTest.dataEnabled.launchIn(this)
+
+ assertThat(underTest.dataEnabled.value).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isDefaultDataSubscription_isDefault() =
+ runBlocking(IMMEDIATE) {
+ connectionsRepo.setDefaultDataSubId(SUB_1_ID)
+
+ var latest: Boolean? = null
+ val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isDefaultDataSubscription_isNotDefault() =
+ runBlocking(IMMEDIATE) {
+ // Our subId is SUB_1_ID
+ connectionsRepo.setDefaultDataSubId(123)
+
+ var latest: Boolean? = null
+ val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() =
+ runBlocking(IMMEDIATE) {
+ val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID"
var latest: Boolean? = null
val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
+ // We don't read the setting directly, we query telephony when changes happen
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+ globalSettings.putInt(subIdSettingName, 0)
+ assertThat(latest).isFalse()
+
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
+ globalSettings.putInt(subIdSettingName, 1)
+ assertThat(latest).isTrue()
+
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+ globalSettings.putInt(subIdSettingName, 0)
assertThat(latest).isFalse()
job.cancel()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
index 326e0d281..a953a3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -16,26 +16,33 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
+import android.content.Intent
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.provider.Settings
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest
+import com.android.internal.telephony.PhoneConstants
import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
@@ -43,7 +50,6 @@
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
-import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -54,32 +60,26 @@
class MobileConnectionsRepositoryTest : SysuiTestCase() {
private lateinit var underTest: MobileConnectionsRepositoryImpl
+ @Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
- @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
private val scope = CoroutineScope(IMMEDIATE)
+ private val globalSettings = FakeSettings()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- whenever(
- broadcastDispatcher.broadcastFlow(
- any(),
- nullable(),
- ArgumentMatchers.anyInt(),
- nullable(),
- )
- )
- .thenReturn(flowOf(Unit))
underTest =
MobileConnectionsRepositoryImpl(
+ connectivityManager,
subscriptionManager,
telephonyManager,
logger,
- broadcastDispatcher,
+ fakeBroadcastDispatcher,
+ globalSettings,
context,
IMMEDIATE,
scope,
@@ -214,6 +214,139 @@
job.cancel()
}
+ @Test
+ fun testDefaultDataSubId_updatesOnBroadcast() =
+ runBlocking(IMMEDIATE) {
+ var latest: Int? = null
+ val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(
+ context,
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+ )
+ }
+
+ assertThat(latest).isEqualTo(SUB_2_ID)
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+ receiver.onReceive(
+ context,
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+ )
+ }
+
+ assertThat(latest).isEqualTo(SUB_1_ID)
+
+ job.cancel()
+ }
+
+ @Test
+ fun mobileConnectivity_default() {
+ assertThat(underTest.defaultMobileNetworkConnectivity.value)
+ .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
+ }
+
+ @Test
+ fun mobileConnectivity_isConnected_isValidated() =
+ runBlocking(IMMEDIATE) {
+ val caps = createCapabilities(connected = true, validated = true)
+
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest)
+ .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true))
+
+ job.cancel()
+ }
+
+ @Test
+ fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() =
+ runBlocking(IMMEDIATE) {
+ var produced = false
+ val job =
+ underTest.globalMobileDataSettingChangedEvent
+ .onEach { produced = true }
+ .launchIn(this)
+
+ assertThat(produced).isFalse()
+
+ globalSettings.putInt(Settings.Global.MOBILE_DATA, 0)
+
+ assertThat(produced).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun mobileConnectivity_isConnected_isNotValidated() =
+ runBlocking(IMMEDIATE) {
+ val caps = createCapabilities(connected = true, validated = false)
+
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest)
+ .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false))
+
+ job.cancel()
+ }
+
+ @Test
+ fun mobileConnectivity_isNotConnected_isNotValidated() =
+ runBlocking(IMMEDIATE) {
+ val caps = createCapabilities(connected = false, validated = false)
+
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest)
+ .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false))
+
+ job.cancel()
+ }
+
+ /** In practice, I don't think this state can ever happen (!connected, validated) */
+ @Test
+ fun mobileConnectivity_isNotConnected_isValidated() =
+ runBlocking(IMMEDIATE) {
+ val caps = createCapabilities(connected = false, validated = true)
+
+ var latest: MobileConnectivityModel? = null
+ val job =
+ underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest).isEqualTo(MobileConnectivityModel(false, true))
+
+ job.cancel()
+ }
+
+ private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected)
+ whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated)
+ }
+
+ private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+ val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
+ verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
+ return callbackCaptor.value!!
+ }
+
private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
verify(subscriptionManager)
@@ -242,5 +375,8 @@
private const val SUB_2_ID = 2
private val SUB_2 =
mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+
+ private const val NET_ID = 123
+ private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 5611c44..3ae7d3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -28,18 +28,23 @@
private val _isEmergencyOnly = MutableStateFlow(false)
override val isEmergencyOnly = _isEmergencyOnly
+ private val _isFailedConnection = MutableStateFlow(false)
+ override val isDefaultConnectionFailed = _isFailedConnection
+
+ override val isDataConnected = MutableStateFlow(true)
+
private val _isDataEnabled = MutableStateFlow(true)
override val isDataEnabled = _isDataEnabled
+ private val _isDefaultDataEnabled = MutableStateFlow(true)
+ override val isDefaultDataEnabled = _isDefaultDataEnabled
+
private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
override val level = _level
private val _numberOfLevels = MutableStateFlow(4)
override val numberOfLevels = _numberOfLevels
- private val _cutOut = MutableStateFlow(false)
- override val cutOut = _cutOut
-
fun setIconGroup(group: SignalIcon.MobileIconGroup) {
_iconGroup.value = group
}
@@ -52,6 +57,14 @@
_isDataEnabled.value = enabled
}
+ fun setIsDefaultDataEnabled(disabled: Boolean) {
+ _isDefaultDataEnabled.value = disabled
+ }
+
+ fun setIsFailedConnection(failed: Boolean) {
+ _isFailedConnection.value = failed
+ }
+
fun setLevel(level: Int) {
_level.value = level
}
@@ -59,8 +72,4 @@
fun setNumberOfLevels(num: Int) {
_numberOfLevels.value = num
}
-
- fun setCutOut(cutOut: Boolean) {
- _cutOut.value = cutOut
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 2bd2286..061c3b54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -26,8 +26,7 @@
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
-class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) :
- MobileIconsInteractor {
+class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor {
val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
val LTE_KEY = mobileMappings.toIconKey(LTE)
val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
@@ -46,9 +45,14 @@
FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
)
+ override val isDefaultConnectionFailed = MutableStateFlow(false)
+
private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
override val filteredSubscriptions = _filteredSubscriptions
+ private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false)
+ override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled
+
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
override val defaultMobileIconMapping = _defaultMobileIconMapping
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index ff44af4..7fc1c0f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -23,6 +23,7 @@
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
@@ -34,6 +35,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -49,12 +51,17 @@
private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
private val connectionRepository = FakeMobileConnectionRepository()
+ private val scope = CoroutineScope(IMMEDIATE)
+
@Before
fun setUp() {
underTest =
MobileIconInteractorImpl(
+ scope,
+ mobileIconsInteractor.activeDataConnectionHasDataEnabled,
mobileIconsInteractor.defaultMobileIconMapping,
mobileIconsInteractor.defaultMobileIconGroup,
+ mobileIconsInteractor.isDefaultConnectionFailed,
mobileMappingsProxy,
connectionRepository,
)
@@ -196,6 +203,66 @@
job.cancel()
}
+ @Test
+ fun test_isDefaultDataEnabled_matchesParent() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this)
+
+ mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
+ assertThat(latest).isTrue()
+
+ mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun test_isDefaultConnectionFailed_matchedParent() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.isDefaultConnectionFailed.launchIn(this)
+
+ mobileIconsInteractor.isDefaultConnectionFailed.value = false
+ assertThat(underTest.isDefaultConnectionFailed.value).isFalse()
+
+ mobileIconsInteractor.isDefaultConnectionFailed.value = true
+ assertThat(underTest.isDefaultConnectionFailed.value).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataState_connected() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected)
+ )
+ yield()
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataState_notConnected() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+
+ connectionRepository.setMobileSubscriptionModel(
+ MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected)
+ )
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 877ce0e..b56dcd7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -17,8 +17,10 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
@@ -32,6 +34,7 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -168,6 +171,92 @@
job.cancel()
}
+ @Test
+ fun activeDataConnection_turnedOn() =
+ runBlocking(IMMEDIATE) {
+ CONNECTION_1.setDataEnabled(true)
+ var latest: Boolean? = null
+ val job =
+ underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun activeDataConnection_turnedOff() =
+ runBlocking(IMMEDIATE) {
+ CONNECTION_1.setDataEnabled(true)
+ var latest: Boolean? = null
+ val job =
+ underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+ CONNECTION_1.setDataEnabled(false)
+ yield()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun activeDataConnection_invalidSubId() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job =
+ underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
+ yield()
+
+ // An invalid active subId should tell us that data is off
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun failedConnection_connected_validated_notFailed() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
+ yield()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun failedConnection_notConnected_notValidated_notFailed() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false))
+ yield()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun failedConnection_connected_notValidated_failed() =
+ runBlocking(IMMEDIATE) {
+ var latest: Boolean? = null
+ val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+
+ connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false))
+ yield()
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index ce0f33f..d4c2c3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -46,10 +46,12 @@
MockitoAnnotations.initMocks(this)
interactor.apply {
setLevel(1)
- setCutOut(false)
+ setIsDefaultDataEnabled(true)
+ setIsFailedConnection(false)
setIconGroup(THREE_G)
setIsEmergencyOnly(false)
setNumberOfLevels(4)
+ isDataConnected.value = true
}
underTest = MobileIconViewModel(SUB_1_ID, interactor, logger)
}
@@ -59,8 +61,23 @@
runBlocking(IMMEDIATE) {
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+ val expected = defaultSignal()
- assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false))
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun iconId_cutout_whenDefaultDataDisabled() =
+ runBlocking(IMMEDIATE) {
+ interactor.setIsDefaultDataEnabled(false)
+
+ var latest: Int? = null
+ val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+ val expected = defaultSignal(level = 1, connected = false)
+
+ assertThat(latest).isEqualTo(expected)
job.cancel()
}
@@ -97,6 +114,44 @@
}
@Test
+ fun networkType_nullWhenFailedConnection() =
+ runBlocking(IMMEDIATE) {
+ interactor.setIconGroup(THREE_G)
+ interactor.setIsDataEnabled(true)
+ interactor.setIsFailedConnection(true)
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_nullWhenDataDisconnects() =
+ runBlocking(IMMEDIATE) {
+ val initial =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription)
+ )
+
+ interactor.setIconGroup(THREE_G)
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.setIconGroup(THREE_G)
+ assertThat(latest).isEqualTo(initial)
+
+ interactor.isDataConnected.value = false
+ yield()
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
fun networkType_null_changeToDisabled() =
runBlocking(IMMEDIATE) {
val expected =
@@ -119,6 +174,14 @@
job.cancel()
}
+ /** Convenience constructor for these tests */
+ private fun defaultSignal(
+ level: Int = 1,
+ connected: Boolean = true,
+ ): Int {
+ return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private const val SUB_1_ID = 1
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 47b4156..e3ae03c 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -3651,6 +3651,10 @@
throw new IllegalArgumentException("The display " + displayId + " does not exist or is"
+ " not tracked by accessibility.");
}
+ if (mProxyManager.isProxyed(displayId)) {
+ throw new IllegalArgumentException("The display " + displayId + " is already being"
+ + "proxy-ed");
+ }
mProxyManager.registerProxy(client, displayId);
return true;
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 934b665..247f320 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -32,6 +32,7 @@
import android.os.IBinder;
import android.os.RemoteCallback;
import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityDisplayProxy;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.Nullable;
@@ -44,7 +45,7 @@
import java.util.Set;
/**
- * Represents the system connection to an {@link android.view.accessibility.AccessibilityProxy}.
+ * Represents the system connection to an {@link AccessibilityDisplayProxy}.
*
* <p>Most methods are no-ops since this connection does not need to capture input or listen to
* hardware-related changes.
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index fb0b8f3..a2ce610 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -16,6 +16,8 @@
package com.android.server.accessibility;
import android.accessibilityservice.IAccessibilityServiceClient;
+import java.util.HashSet;
+
/**
* Manages proxy connections.
*
@@ -26,6 +28,7 @@
*/
public class ProxyManager {
private final Object mLock;
+ private final HashSet<Integer> mDisplayIds = new HashSet<>();
ProxyManager(Object lock) {
mLock = lock;
@@ -35,12 +38,21 @@
* TODO: Create the proxy service connection.
*/
public void registerProxy(IAccessibilityServiceClient client, int displayId) {
+ mDisplayIds.add(displayId);
}
/**
* TODO: Unregister the proxy service connection based on display id.
*/
public boolean unregisterProxy(int displayId) {
+ mDisplayIds.remove(displayId);
return true;
}
+
+ /**
+ * Checks if a display id is being proxy-ed.
+ */
+ public boolean isProxyed(int displayId) {
+ return mDisplayIds.contains(displayId);
+ }
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 47ce592..64b7688 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -24,6 +24,7 @@
import static android.service.autofill.FillEventHistory.Event.UI_TYPE_UNKNOWN;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE;
+import static android.service.autofill.FillRequest.FLAG_RESET_FILL_DIALOG_STATE;
import static android.service.autofill.FillRequest.FLAG_SUPPORTS_FILL_DIALOG;
import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
@@ -416,6 +417,14 @@
@GuardedBy("mLock")
private boolean mPreviouslyFillDialogPotentiallyStarted;
+ /**
+ * Keeps the fill dialog trigger ids of the last response. This invalidates
+ * the trigger ids of the previous response.
+ */
+ @Nullable
+ @GuardedBy("mLock")
+ private AutofillId[] mLastFillDialogTriggerIds;
+
void onSwitchInputMethodLocked() {
// One caveat is that for the case where the focus is on a field for which regular autofill
// returns null, and augmented autofill is triggered, and then the user switches the input
@@ -1222,6 +1231,8 @@
return;
}
+ mLastFillDialogTriggerIds = response.getFillDialogTriggerIds();
+
final int flags = response.getFlags();
if ((flags & FillResponse.FLAG_DELAY_FILL) != 0) {
Slog.v(TAG, "Service requested to wait for delayed fill response.");
@@ -1310,6 +1321,7 @@
// fallback to the default platform password manager
mSessionFlags.mClientSuggestionsEnabled = false;
+ mLastFillDialogTriggerIds = null;
final InlineSuggestionsRequest inlineRequest =
(mLastInlineSuggestionsRequest != null
@@ -1348,6 +1360,7 @@
+ (timedOut ? "timeout" : "failure"));
}
mService.resetLastResponse();
+ mLastFillDialogTriggerIds = null;
final LogMaker requestLog = mRequestLogs.get(requestId);
if (requestLog == null) {
Slog.w(TAG, "onFillRequestFailureOrTimeout(): no log for id " + requestId);
@@ -3049,6 +3062,11 @@
}
}
+ if ((flags & FLAG_RESET_FILL_DIALOG_STATE) != 0) {
+ if (sDebug) Log.d(TAG, "force to reset fill dialog state");
+ mSessionFlags.mFillDialogDisabled = false;
+ }
+
switch(action) {
case ACTION_START_SESSION:
// View is triggering autofill.
@@ -3488,10 +3506,8 @@
}
private boolean isFillDialogUiEnabled() {
- // TODO read from Settings or somewhere
- final boolean isSettingsEnabledFillDialog = true;
synchronized (mLock) {
- return isSettingsEnabledFillDialog && !mSessionFlags.mFillDialogDisabled;
+ return !mSessionFlags.mFillDialogDisabled;
}
}
@@ -3517,14 +3533,25 @@
AutofillId filledId, String filterText, int flags) {
if (!isFillDialogUiEnabled()) {
// Unsupported fill dialog UI
+ if (sDebug) Log.w(TAG, "requestShowFillDialog: fill dialog is disabled");
return false;
}
if ((flags & FillRequest.FLAG_IME_SHOWING) != 0) {
// IME is showing, fallback to normal suggestions UI
+ if (sDebug) Log.w(TAG, "requestShowFillDialog: IME is showing");
return false;
}
+ synchronized (mLock) {
+ if (mLastFillDialogTriggerIds == null
+ || !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
+ // Last fill dialog triggered ids are changed.
+ if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+ return false;
+ }
+ }
+
final Drawable serviceIcon = getServiceIcon();
getUiForShowing().showFillDialog(filledId, response, filterText,
@@ -4394,6 +4421,13 @@
if (mSessionFlags.mAugmentedAutofillOnly) {
pw.print(prefix); pw.println("For Augmented Autofill Only");
}
+ if (mSessionFlags.mFillDialogDisabled) {
+ pw.print(prefix); pw.println("Fill Dialog disabled");
+ }
+ if (mLastFillDialogTriggerIds != null) {
+ pw.print(prefix); pw.println("Last Fill Dialog trigger ids: ");
+ pw.println(mSelectedDatasetIds);
+ }
if (mAugmentedAutofillDestroyer != null) {
pw.print(prefix); pw.println("has mAugmentedAutofillDestroyer");
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2eaddb1..062afe9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -13874,6 +13874,29 @@
@Nullable IBinder backgroundActivityStartsToken,
@Nullable int[] broadcastAllowList,
@Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
+ final int cookie = BroadcastQueue.traceBegin("broadcastIntentLockedTraced");
+ final int res = broadcastIntentLockedTraced(callerApp, callerPackage, callerFeatureId,
+ intent, resolvedType, resultToApp, resultTo, resultCode, resultData, resultExtras,
+ requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
+ ordered, sticky, callingPid, callingUid, realCallingUid, realCallingPid, userId,
+ allowBackgroundActivityStarts, backgroundActivityStartsToken, broadcastAllowList,
+ filterExtrasForReceiver);
+ BroadcastQueue.traceEnd(cookie);
+ return res;
+ }
+
+ @GuardedBy("this")
+ final int broadcastIntentLockedTraced(ProcessRecord callerApp, String callerPackage,
+ @Nullable String callerFeatureId, Intent intent, String resolvedType,
+ ProcessRecord resultToApp, IIntentReceiver resultTo, int resultCode, String resultData,
+ Bundle resultExtras, String[] requiredPermissions,
+ String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
+ boolean ordered, boolean sticky, int callingPid, int callingUid,
+ int realCallingUid, int realCallingPid, int userId,
+ boolean allowBackgroundActivityStarts,
+ @Nullable IBinder backgroundActivityStartsToken,
+ @Nullable int[] broadcastAllowList,
+ @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
// Ensure all internal loopers are registered for idle checks
BroadcastLoopers.addMyLooper();
@@ -14425,6 +14448,7 @@
}
// Figure out who all will receive this broadcast.
+ final int cookie = BroadcastQueue.traceBegin("queryReceivers");
List receivers = null;
List<BroadcastFilter> registeredReceivers = null;
// Need to resolve the intent to interested receivers...
@@ -14455,6 +14479,7 @@
resolvedType, false /*defaultOnly*/, userId);
}
}
+ BroadcastQueue.traceEnd(cookie);
final boolean replacePending =
(intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
diff --git a/services/core/java/com/android/server/am/BroadcastLoopers.java b/services/core/java/com/android/server/am/BroadcastLoopers.java
index bebb484..b828720 100644
--- a/services/core/java/com/android/server/am/BroadcastLoopers.java
+++ b/services/core/java/com/android/server/am/BroadcastLoopers.java
@@ -25,6 +25,8 @@
import android.util.ArraySet;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
@@ -37,6 +39,7 @@
public class BroadcastLoopers {
private static final String TAG = "BroadcastLoopers";
+ @GuardedBy("sLoopers")
private static final ArraySet<Looper> sLoopers = new ArraySet<>();
/**
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index f7d24e9..2e12309 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -114,7 +114,14 @@
* dispatched to this process, in the same representation as
* {@link #mPending}.
*/
- private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>();
+ private final ArrayDeque<SomeArgs> mPendingUrgent = new ArrayDeque<>(4);
+
+ /**
+ * Ordered collection of "offload" broadcasts that are waiting to be
+ * dispatched to this process, in the same representation as
+ * {@link #mPending}.
+ */
+ private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);
/**
* Broadcast actively being dispatched to this process.
@@ -148,8 +155,7 @@
private boolean mActiveViaColdStart;
/**
- * Count of {@link #mPending} and {@link #mPendingUrgent} broadcasts of
- * these various flavors.
+ * Count of pending broadcasts of these various flavors.
*/
private int mCountForeground;
private int mCountOrdered;
@@ -177,6 +183,16 @@
this.uid = uid;
}
+ private @NonNull ArrayDeque<SomeArgs> getQueueForBroadcast(@NonNull BroadcastRecord record) {
+ if (record.isUrgent()) {
+ return mPendingUrgent;
+ } else if (record.isOffload()) {
+ return mPendingOffload;
+ } else {
+ return mPending;
+ }
+ }
+
/**
* Enqueue the given broadcast to be dispatched to this process at some
* future point in time. The target receiver is indicated by the given index
@@ -193,10 +209,12 @@
public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
int blockedUntilTerminalCount) {
if (record.isReplacePending()) {
- boolean didReplace = replaceBroadcastInQueue(mPending,
- record, recordIndex, blockedUntilTerminalCount)
- || replaceBroadcastInQueue(mPendingUrgent,
- record, recordIndex, blockedUntilTerminalCount);
+ boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex,
+ blockedUntilTerminalCount)
+ || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex,
+ blockedUntilTerminalCount)
+ || replaceBroadcastInQueue(mPendingOffload, record, recordIndex,
+ blockedUntilTerminalCount);
if (didReplace) {
return;
}
@@ -213,8 +231,7 @@
// issued ahead of others that are already pending, for example if this new
// broadcast is in a different delivery class or is tied to a direct user interaction
// with implicit responsiveness expectations.
- final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending;
- queue.addLast(newBroadcastArgs);
+ getQueueForBroadcast(record).addLast(newBroadcastArgs);
onBroadcastEnqueued(record, recordIndex);
}
@@ -227,7 +244,7 @@
* {@code false} otherwise.
*/
private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
- @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) {
+ @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) {
final Iterator<SomeArgs> it = queue.descendingIterator();
final Object receiver = record.receivers.get(recordIndex);
while (it.hasNext()) {
@@ -279,10 +296,13 @@
*/
public boolean forEachMatchingBroadcast(@NonNull BroadcastPredicate predicate,
@NonNull BroadcastConsumer consumer, boolean andRemove) {
- boolean didSomething = forEachMatchingBroadcastInQueue(mPending,
+ boolean didSomething = false;
+ didSomething |= forEachMatchingBroadcastInQueue(mPending,
predicate, consumer, andRemove);
didSomething |= forEachMatchingBroadcastInQueue(mPendingUrgent,
predicate, consumer, andRemove);
+ didSomething |= forEachMatchingBroadcastInQueue(mPendingOffload,
+ predicate, consumer, andRemove);
return didSomething;
}
@@ -516,7 +536,7 @@
}
public boolean isEmpty() {
- return mPending.isEmpty() && mPendingUrgent.isEmpty();
+ return mPending.isEmpty() && mPendingUrgent.isEmpty() && mPendingOffload.isEmpty();
}
public boolean isActive() {
@@ -537,6 +557,8 @@
return mPendingUrgent;
} else if (!mPending.isEmpty()) {
return mPending;
+ } else if (!mPendingOffload.isEmpty()) {
+ return mPendingOffload;
}
return null;
}
@@ -581,12 +603,15 @@
}
final SomeArgs next = mPending.peekFirst();
final SomeArgs nextUrgent = mPendingUrgent.peekFirst();
+ final SomeArgs nextOffload = mPendingOffload.peekFirst();
// Empty queue is past any barrier
- final boolean nextLater = next == null
+ final boolean nextLater = (next == null)
|| ((BroadcastRecord) next.arg1).enqueueTime > barrierTime;
- final boolean nextUrgentLater = nextUrgent == null
+ final boolean nextUrgentLater = (nextUrgent == null)
|| ((BroadcastRecord) nextUrgent.arg1).enqueueTime > barrierTime;
- return nextLater && nextUrgentLater;
+ final boolean nextOffloadLater = (nextOffload == null)
+ || ((BroadcastRecord) nextOffload.arg1).enqueueTime > barrierTime;
+ return nextLater && nextUrgentLater && nextOffloadLater;
}
public boolean isRunnable() {
@@ -726,8 +751,9 @@
// If we have too many broadcasts pending, bypass any delays that
// might have been applied above to aid draining
- if (mPending.size() + mPendingUrgent.size() >= constants.MAX_PENDING_BROADCASTS) {
- mRunnableAt = runnableAt;
+ if (mPending.size() + mPendingUrgent.size()
+ + mPendingOffload.size() >= constants.MAX_PENDING_BROADCASTS) {
+ mRunnableAt = Math.min(mRunnableAt, runnableAt);
mRunnableAtReason = REASON_MAX_PENDING;
}
} else {
@@ -845,23 +871,28 @@
pw.println();
pw.increaseIndent();
if (mActive != null) {
- dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount);
+ dumpRecord("ACTIVE", now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount);
}
for (SomeArgs args : mPendingUrgent) {
final BroadcastRecord r = (BroadcastRecord) args.arg1;
- dumpRecord(now, pw, r, args.argi1, args.argi2);
+ dumpRecord("URGENT", now, pw, r, args.argi1, args.argi2);
}
for (SomeArgs args : mPending) {
final BroadcastRecord r = (BroadcastRecord) args.arg1;
- dumpRecord(now, pw, r, args.argi1, args.argi2);
+ dumpRecord(null, now, pw, r, args.argi1, args.argi2);
+ }
+ for (SomeArgs args : mPendingOffload) {
+ final BroadcastRecord r = (BroadcastRecord) args.arg1;
+ dumpRecord("OFFLOAD", now, pw, r, args.argi1, args.argi2);
}
pw.decreaseIndent();
pw.println();
}
@NeverCompile
- private void dumpRecord(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw,
- @NonNull BroadcastRecord record, int recordIndex, int blockedUntilTerminalCount) {
+ private void dumpRecord(@Nullable String flavor, @UptimeMillisLong long now,
+ @NonNull IndentingPrintWriter pw, @NonNull BroadcastRecord record, int recordIndex,
+ int blockedUntilTerminalCount) {
TimeUtils.formatDuration(record.enqueueTime, now, pw);
pw.print(' ');
pw.println(record.toShortString());
@@ -872,6 +903,10 @@
pw.print(" at ");
TimeUtils.formatDuration(record.scheduledTime[recordIndex], now, pw);
}
+ if (flavor != null) {
+ pw.print(' ');
+ pw.print(flavor);
+ }
final Object receiver = record.receivers.get(recordIndex);
if (receiver instanceof BroadcastFilter) {
final BroadcastFilter filter = (BroadcastFilter) receiver;
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 1e172fc..e0fab2c 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -24,6 +24,7 @@
import android.os.Bundle;
import android.os.DropBoxManager;
import android.os.Handler;
+import android.os.Trace;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
@@ -76,6 +77,18 @@
}
}
+ static int traceBegin(@NonNull String methodName) {
+ final int cookie = methodName.hashCode();
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ TAG, methodName, cookie);
+ return cookie;
+ }
+
+ static void traceEnd(int cookie) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ TAG, cookie);
+ }
+
@Override
public String toString() {
return mQueueName;
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 5750619..af2a97e 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -64,7 +64,6 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
-import android.os.Trace;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.IndentingPrintWriter;
@@ -144,14 +143,6 @@
mRunning = new BroadcastProcessQueue[mConstants.MAX_RUNNING_PROCESS_QUEUES];
}
- // TODO: add support for replacing pending broadcasts
- // TODO: add support for merging pending broadcasts
-
- // TODO: consider reordering foreground broadcasts within queue
-
- // TODO: pause queues when background services are running
- // TODO: pause queues when processes are frozen
-
/**
* Map from UID to per-process broadcast queues. If a UID hosts more than
* one process, each additional process is stored as a linked list using
@@ -222,12 +213,22 @@
private static final int MSG_DELIVERY_TIMEOUT_HARD = 3;
private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 4;
private static final int MSG_CHECK_HEALTH = 5;
+ private static final int MSG_FINISH_RECEIVER = 6;
private void enqueueUpdateRunningList() {
mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
mLocalHandler.sendEmptyMessage(MSG_UPDATE_RUNNING_LIST);
}
+ private void enqueueFinishReceiver(@NonNull BroadcastProcessQueue queue,
+ @DeliveryState int deliveryState, @NonNull String reason) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.arg1 = queue;
+ args.argi1 = deliveryState;
+ args.arg2 = reason;
+ mLocalHandler.sendMessage(Message.obtain(mLocalHandler, MSG_FINISH_RECEIVER, args));
+ }
+
private final Handler mLocalHandler;
private final Handler.Callback mLocalCallback = (msg) -> {
@@ -266,6 +267,17 @@
}
return true;
}
+ case MSG_FINISH_RECEIVER: {
+ synchronized (mService) {
+ final SomeArgs args = (SomeArgs) msg.obj;
+ final BroadcastProcessQueue queue = (BroadcastProcessQueue) args.arg1;
+ final int deliveryState = args.argi1;
+ final String reason = (String) args.arg2;
+ args.recycle();
+ finishReceiverLocked(queue, deliveryState, reason);
+ }
+ return true;
+ }
}
return false;
};
@@ -309,6 +321,7 @@
return;
}
+ final int cookie = traceBegin("updateRunnableList");
final boolean wantQueue = queue.isRunnable();
final boolean inQueue = (queue == mRunnableHead) || (queue.runnableAtPrev != null)
|| (queue.runnableAtNext != null);
@@ -335,6 +348,8 @@
if (queue.isEmpty() && !queue.isActive() && !queue.isProcessWarm()) {
removeProcessQueue(queue.processName, queue.uid);
}
+
+ traceEnd(cookie);
}
/**
@@ -349,7 +364,7 @@
int avail = mRunning.length - getRunningSize();
if (avail == 0) return;
- final int cookie = traceBegin(TAG, "updateRunningList");
+ final int cookie = traceBegin("updateRunningList");
final long now = SystemClock.uptimeMillis();
// If someone is waiting for a state, everything is runnable now
@@ -449,7 +464,7 @@
});
}
- traceEnd(TAG, cookie);
+ traceEnd(cookie);
}
@Override
@@ -516,7 +531,8 @@
if (queue != null) {
// If queue was running a broadcast, fail it
if (queue.isActive()) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+ finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
+ "onApplicationCleanupLocked");
}
// Skip any pending registered receivers, since the old process
@@ -544,6 +560,7 @@
public void enqueueBroadcastLocked(@NonNull BroadcastRecord r) {
if (DEBUG_BROADCAST) logv("Enqueuing " + r + " for " + r.receivers.size() + " receivers");
+ final int cookie = traceBegin("enqueueBroadcast");
r.applySingletonPolicy(mService);
final IntentFilter removeMatchingFilter = (r.options != null)
@@ -613,6 +630,8 @@
if (r.receivers.isEmpty()) {
scheduleResultTo(r);
}
+
+ traceEnd(cookie);
}
private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
@@ -668,7 +687,8 @@
// Ignore registered receivers from a previous PID
if (receiver instanceof BroadcastFilter) {
mRunningColdStart = null;
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
+ "BroadcastFilter for cold app");
return;
}
@@ -690,7 +710,8 @@
hostingRecord, zygotePolicyFlags, allowWhileBooting, false);
if (queue.app == null) {
mRunningColdStart = null;
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE,
+ "startProcessLocked failed");
return;
}
}
@@ -721,33 +742,37 @@
// If someone already finished this broadcast, finish immediately
final int oldDeliveryState = getDeliveryState(r, index);
if (isDeliveryStateTerminal(oldDeliveryState)) {
- finishReceiverLocked(queue, oldDeliveryState);
+ enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state");
return;
}
// Consider additional cases where we'd want to finish immediately
if (app.isInFullBackup()) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
return;
}
if (mSkipPolicy.shouldSkip(r, receiver)) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
return;
}
final Intent receiverIntent = r.getReceiverIntent(receiver);
if (receiverIntent == null) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
return;
}
// Ignore registered receivers from a previous PID
if ((receiver instanceof BroadcastFilter)
&& ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
+ "BroadcastFilter for mismatched PID");
return;
}
- if (mService.mProcessesReady && !r.timeoutExempt) {
+ // Skip ANR tracking early during boot, when requested, or when we
+ // immediately assume delivery success
+ final boolean assumeDelivered = (receiver instanceof BroadcastFilter) && !r.ordered;
+ if (mService.mProcessesReady && !r.timeoutExempt && !assumeDelivered) {
queue.lastCpuDelayTime = queue.app.getCpuDelayTime();
final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT;
@@ -775,7 +800,8 @@
}
if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app);
- setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED);
+ setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED,
+ "scheduleReceiverWarmLocked");
final IApplicationThread thread = app.getOnewayThread();
if (thread != null) {
@@ -789,8 +815,9 @@
// TODO: consider making registered receivers of unordered
// broadcasts report results to detect ANRs
- if (!r.ordered) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
+ if (assumeDelivered) {
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_DELIVERED,
+ "assuming delivered");
}
} else {
notifyScheduleReceiver(app, r, (ResolveInfo) receiver);
@@ -804,10 +831,11 @@
logw(msg);
app.scheduleCrashLocked(msg, CannotDeliverBroadcastException.TYPE_ID, null);
app.setKilled(true);
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, "remote app");
}
} else {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
+ enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE,
+ "missing IApplicationThread");
}
}
@@ -851,7 +879,8 @@
}
private void deliveryTimeoutHardLocked(@NonNull BroadcastProcessQueue queue) {
- finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT);
+ finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT,
+ "deliveryTimeoutHardLocked");
}
@Override
@@ -878,16 +907,17 @@
if (r.resultAbort) {
for (int i = r.terminalCount + 1; i < r.receivers.size(); i++) {
setDeliveryState(null, null, r, i, r.receivers.get(i),
- BroadcastRecord.DELIVERY_SKIPPED);
+ BroadcastRecord.DELIVERY_SKIPPED, "resultAbort");
}
}
}
- return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
+ return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
}
private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
- @DeliveryState int deliveryState) {
+ @DeliveryState int deliveryState, @NonNull String reason) {
+ final int cookie = traceBegin("finishReceiver");
checkState(queue.isActive(), "isActive");
final ProcessRecord app = queue.app;
@@ -895,7 +925,7 @@
final int index = queue.getActiveIndex();
final Object receiver = r.receivers.get(index);
- setDeliveryState(queue, app, r, index, receiver, deliveryState);
+ setDeliveryState(queue, app, r, index, receiver, deliveryState, reason);
if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) {
r.anrCount++;
@@ -914,11 +944,12 @@
final boolean shouldRetire =
(queue.getActiveCountSinceIdle() >= mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
+ final boolean res;
if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
// We're on a roll; move onto the next broadcast for this process
queue.makeActiveNextPending();
scheduleReceiverWarmLocked(queue);
- return true;
+ res = true;
} else {
// We've drained running broadcasts; maybe move back to runnable
queue.makeActiveIdle();
@@ -932,8 +963,10 @@
// Tell other OS components that app is not actively running, giving
// a chance to update OOM adjustment
notifyStoppedRunning(queue);
- return false;
+ res = false;
}
+ traceEnd(cookie);
+ return res;
}
/**
@@ -942,7 +975,8 @@
*/
private void setDeliveryState(@Nullable BroadcastProcessQueue queue,
@Nullable ProcessRecord app, @NonNull BroadcastRecord r, int index,
- @NonNull Object receiver, @DeliveryState int newDeliveryState) {
+ @NonNull Object receiver, @DeliveryState int newDeliveryState, String reason) {
+ final int cookie = traceBegin("setDeliveryState");
final int oldDeliveryState = getDeliveryState(r, index);
// Only apply state when we haven't already reached a terminal state;
@@ -970,7 +1004,7 @@
logw("Delivery state of " + r + " to " + receiver
+ " via " + app + " changed from "
+ deliveryStateToString(oldDeliveryState) + " to "
- + deliveryStateToString(newDeliveryState));
+ + deliveryStateToString(newDeliveryState) + " because " + reason);
}
r.terminalCount++;
@@ -1000,6 +1034,8 @@
enqueueUpdateRunningList();
}
}
+
+ traceEnd(cookie);
}
private @DeliveryState int getDeliveryState(@NonNull BroadcastRecord r, int index) {
@@ -1060,7 +1096,8 @@
* of it matching a predicate.
*/
private final BroadcastConsumer mBroadcastConsumerSkip = (r, i) -> {
- setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+ setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED,
+ "mBroadcastConsumerSkip");
};
/**
@@ -1068,7 +1105,8 @@
* cancelled, usually as a result of it matching a predicate.
*/
private final BroadcastConsumer mBroadcastConsumerSkipAndCanceled = (r, i) -> {
- setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+ setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED,
+ "mBroadcastConsumerSkipAndCanceled");
r.resultCode = Activity.RESULT_CANCELED;
r.resultData = null;
r.resultExtras = null;
@@ -1260,18 +1298,6 @@
}
}
- private int traceBegin(String trackName, String methodName) {
- final int cookie = methodName.hashCode();
- Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- trackName, methodName, cookie);
- return cookie;
- }
-
- private void traceEnd(String trackName, int cookie) {
- Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- trackName, cookie);
- }
-
private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) {
if (!queue.isProcessWarm()) {
queue.setProcess(mService.getProcessRecordLocked(queue.processName, queue.uid));
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 2a3c897..65f9b9b 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -617,6 +617,10 @@
return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
}
+ boolean isOffload() {
+ return (intent.getFlags() & Intent.FLAG_RECEIVER_OFFLOAD) != 0;
+ }
+
/**
* Core policy determination about this broadcast's delivery prioritization
*/
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index 60fddf0..481ab17 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -21,6 +21,7 @@
import static com.android.server.am.BroadcastQueue.TAG;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.AppOpsManager;
@@ -42,6 +43,8 @@
import com.android.internal.util.ArrayUtils;
+import java.util.Objects;
+
/**
* Policy logic that decides if delivery of a particular {@link BroadcastRecord}
* should be skipped for a given {@link ResolveInfo} or {@link BroadcastFilter}.
@@ -51,8 +54,8 @@
public class BroadcastSkipPolicy {
private final ActivityManagerService mService;
- public BroadcastSkipPolicy(ActivityManagerService service) {
- mService = service;
+ public BroadcastSkipPolicy(@NonNull ActivityManagerService service) {
+ mService = Objects.requireNonNull(service);
}
/**
@@ -60,18 +63,39 @@
* the given {@link BroadcastFilter} or {@link ResolveInfo}.
*/
public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull Object target) {
- if (target instanceof BroadcastFilter) {
- return shouldSkip(r, (BroadcastFilter) target);
+ final String msg = shouldSkipMessage(r, target);
+ if (msg != null) {
+ Slog.w(TAG, msg);
+ return true;
} else {
- return shouldSkip(r, (ResolveInfo) target);
+ return false;
+ }
+ }
+
+ /**
+ * Determine if the given {@link BroadcastRecord} is eligible to be sent to
+ * the given {@link BroadcastFilter} or {@link ResolveInfo}.
+ *
+ * @return message indicating why the argument should be skipped, otherwise
+ * {@code null} if it can proceed.
+ */
+ public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+ if (target instanceof BroadcastFilter) {
+ return shouldSkipMessage(r, (BroadcastFilter) target);
+ } else {
+ return shouldSkipMessage(r, (ResolveInfo) target);
}
}
/**
* Determine if the given {@link BroadcastRecord} is eligible to be sent to
* the given {@link ResolveInfo}.
+ *
+ * @return message indicating why the argument should be skipped, otherwise
+ * {@code null} if it can proceed.
*/
- public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull ResolveInfo info) {
+ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
+ @NonNull ResolveInfo info) {
final BroadcastOptions brOptions = r.options;
final ComponentName component = new ComponentName(
info.activityInfo.applicationInfo.packageName,
@@ -82,58 +106,52 @@
< brOptions.getMinManifestReceiverApiLevel() ||
info.activityInfo.applicationInfo.targetSdkVersion
> brOptions.getMaxManifestReceiverApiLevel())) {
- Slog.w(TAG, "Target SDK mismatch: receiver " + info.activityInfo
+ return "Target SDK mismatch: receiver " + info.activityInfo
+ " targets " + info.activityInfo.applicationInfo.targetSdkVersion
+ " but delivery restricted to ["
+ brOptions.getMinManifestReceiverApiLevel() + ", "
+ brOptions.getMaxManifestReceiverApiLevel()
- + "] broadcasting " + broadcastDescription(r, component));
- return true;
+ + "] broadcasting " + broadcastDescription(r, component);
}
if (brOptions != null &&
!brOptions.testRequireCompatChange(info.activityInfo.applicationInfo.uid)) {
- Slog.w(TAG, "Compat change filtered: broadcasting " + broadcastDescription(r, component)
+ return "Compat change filtered: broadcasting " + broadcastDescription(r, component)
+ " to uid " + info.activityInfo.applicationInfo.uid + " due to compat change "
- + r.options.getRequireCompatChangeId());
- return true;
+ + r.options.getRequireCompatChangeId();
}
if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid,
component.getPackageName(), info.activityInfo.applicationInfo.uid)) {
- Slog.w(TAG, "Association not allowed: broadcasting "
- + broadcastDescription(r, component));
- return true;
+ return "Association not allowed: broadcasting "
+ + broadcastDescription(r, component);
}
if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid,
r.callingPid, r.resolvedType, info.activityInfo.applicationInfo.uid)) {
- Slog.w(TAG, "Firewall blocked: broadcasting "
- + broadcastDescription(r, component));
- return true;
+ return "Firewall blocked: broadcasting "
+ + broadcastDescription(r, component);
}
int perm = checkComponentPermission(info.activityInfo.permission,
r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid,
info.activityInfo.exported);
if (perm != PackageManager.PERMISSION_GRANTED) {
if (!info.activityInfo.exported) {
- Slog.w(TAG, "Permission Denial: broadcasting "
+ return "Permission Denial: broadcasting "
+ broadcastDescription(r, component)
- + " is not exported from uid " + info.activityInfo.applicationInfo.uid);
+ + " is not exported from uid " + info.activityInfo.applicationInfo.uid;
} else {
- Slog.w(TAG, "Permission Denial: broadcasting "
+ return "Permission Denial: broadcasting "
+ broadcastDescription(r, component)
- + " requires " + info.activityInfo.permission);
+ + " requires " + info.activityInfo.permission;
}
- return true;
} else if (info.activityInfo.permission != null) {
final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
r.callingUid, r.callerPackage, r.callerFeatureId,
"Broadcast delivered to " + info.activityInfo.name)
!= AppOpsManager.MODE_ALLOWED) {
- Slog.w(TAG, "Appop Denial: broadcasting "
+ return "Appop Denial: broadcasting "
+ broadcastDescription(r, component)
+ " requires appop " + AppOpsManager.permissionToOp(
- info.activityInfo.permission));
- return true;
+ info.activityInfo.permission);
}
}
@@ -142,38 +160,34 @@
android.Manifest.permission.INTERACT_ACROSS_USERS,
info.activityInfo.applicationInfo.uid)
!= PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: Receiver " + component.flattenToShortString()
+ return "Permission Denial: Receiver " + component.flattenToShortString()
+ " requests FLAG_SINGLE_USER, but app does not hold "
- + android.Manifest.permission.INTERACT_ACROSS_USERS);
- return true;
+ + android.Manifest.permission.INTERACT_ACROSS_USERS;
}
}
if (info.activityInfo.applicationInfo.isInstantApp()
&& r.callingUid != info.activityInfo.applicationInfo.uid) {
- Slog.w(TAG, "Instant App Denial: receiving "
+ return "Instant App Denial: receiving "
+ r.intent
+ " to " + component.flattenToShortString()
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")"
- + " Instant Apps do not support manifest receivers");
- return true;
+ + " Instant Apps do not support manifest receivers";
}
if (r.callerInstantApp
&& (info.activityInfo.flags & ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP) == 0
&& r.callingUid != info.activityInfo.applicationInfo.uid) {
- Slog.w(TAG, "Instant App Denial: receiving "
+ return "Instant App Denial: receiving "
+ r.intent
+ " to " + component.flattenToShortString()
+ " requires receiver have visibleToInstantApps set"
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
if (r.curApp != null && r.curApp.mErrorState.isCrashing()) {
// If the target process is crashing, just skip it.
- Slog.w(TAG, "Skipping deliver ordered [" + r.queue.toString() + "] " + r
- + " to " + r.curApp + ": process crashing");
- return true;
+ return "Skipping deliver ordered [" + r.queue.toString() + "] " + r
+ + " to " + r.curApp + ": process crashing";
}
boolean isAvailable = false;
@@ -183,15 +197,13 @@
UserHandle.getUserId(info.activityInfo.applicationInfo.uid));
} catch (Exception e) {
// all such failures mean we skip this receiver
- Slog.w(TAG, "Exception getting recipient info for "
- + info.activityInfo.packageName, e);
+ return "Exception getting recipient info for "
+ + info.activityInfo.packageName;
}
if (!isAvailable) {
- Slog.w(TAG,
- "Skipping delivery to " + info.activityInfo.packageName + " / "
+ return "Skipping delivery to " + info.activityInfo.packageName + " / "
+ info.activityInfo.applicationInfo.uid
- + " : package no longer available");
- return true;
+ + " : package no longer available";
}
// If permissions need a review before any of the app components can run, we drop
@@ -201,10 +213,8 @@
if (!requestStartTargetPermissionsReviewIfNeededLocked(r,
info.activityInfo.packageName, UserHandle.getUserId(
info.activityInfo.applicationInfo.uid))) {
- Slog.w(TAG,
- "Skipping delivery: permission review required for "
- + broadcastDescription(r, component));
- return true;
+ return "Skipping delivery: permission review required for "
+ + broadcastDescription(r, component);
}
final int allowed = mService.getAppStartModeLOSP(
@@ -216,10 +226,9 @@
// to it and the app is in a state that should not receive it
// (depending on how getAppStartModeLOSP has determined that).
if (allowed == ActivityManager.APP_START_MODE_DISABLED) {
- Slog.w(TAG, "Background execution disabled: receiving "
+ return "Background execution disabled: receiving "
+ r.intent + " to "
- + component.flattenToShortString());
- return true;
+ + component.flattenToShortString();
} else if (((r.intent.getFlags()&Intent.FLAG_RECEIVER_EXCLUDE_BACKGROUND) != 0)
|| (r.intent.getComponent() == null
&& r.intent.getPackage() == null
@@ -228,10 +237,9 @@
&& !isSignaturePerm(r.requiredPermissions))) {
mService.addBackgroundCheckViolationLocked(r.intent.getAction(),
component.getPackageName());
- Slog.w(TAG, "Background execution not allowed: receiving "
+ return "Background execution not allowed: receiving "
+ r.intent + " to "
- + component.flattenToShortString());
- return true;
+ + component.flattenToShortString();
}
}
@@ -239,10 +247,8 @@
&& !mService.mUserController
.isUserRunning(UserHandle.getUserId(info.activityInfo.applicationInfo.uid),
0 /* flags */)) {
- Slog.w(TAG,
- "Skipping delivery to " + info.activityInfo.packageName + " / "
- + info.activityInfo.applicationInfo.uid + " : user is not running");
- return true;
+ return "Skipping delivery to " + info.activityInfo.packageName + " / "
+ + info.activityInfo.applicationInfo.uid + " : user is not running";
}
if (r.excludedPermissions != null && r.excludedPermissions.length > 0) {
@@ -268,13 +274,15 @@
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName)
== AppOpsManager.MODE_ALLOWED)) {
- return true;
+ return "Skipping delivery to " + info.activityInfo.packageName
+ + " due to excluded permission " + excludedPermission;
}
} else {
// When there is no app op associated with the permission,
// skip when permission is granted.
if (perm == PackageManager.PERMISSION_GRANTED) {
- return true;
+ return "Skipping delivery to " + info.activityInfo.packageName
+ + " due to excluded permission " + excludedPermission;
}
}
}
@@ -283,13 +291,12 @@
// Check that the receiver does *not* belong to any of the excluded packages
if (r.excludedPackages != null && r.excludedPackages.length > 0) {
if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) {
- Slog.w(TAG, "Skipping delivery of excluded package "
+ return "Skipping delivery of excluded package "
+ r.intent + " to "
+ component.flattenToShortString()
+ " excludes package " + component.getPackageName()
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
}
@@ -307,95 +314,94 @@
perm = PackageManager.PERMISSION_DENIED;
}
if (perm != PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: receiving "
+ return "Permission Denial: receiving "
+ r.intent + " to "
+ component.flattenToShortString()
+ " requires " + requiredPermission
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) {
if (!noteOpForManifestReceiver(appOp, r, info, component)) {
- return true;
+ return "Skipping delivery to " + info.activityInfo.packageName
+ + " due to required appop " + appOp;
}
}
}
}
if (r.appOp != AppOpsManager.OP_NONE) {
if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
- return true;
+ return "Skipping delivery to " + info.activityInfo.packageName
+ + " due to required appop " + r.appOp;
}
}
- return false;
+ return null;
}
/**
* Determine if the given {@link BroadcastRecord} is eligible to be sent to
* the given {@link BroadcastFilter}.
+ *
+ * @return message indicating why the argument should be skipped, otherwise
+ * {@code null} if it can proceed.
*/
- public boolean shouldSkip(@NonNull BroadcastRecord r, @NonNull BroadcastFilter filter) {
+ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
+ @NonNull BroadcastFilter filter) {
if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
- Slog.w(TAG, "Compat change filtered: broadcasting " + r.intent.toString()
+ return "Compat change filtered: broadcasting " + r.intent.toString()
+ " to uid " + filter.owningUid + " due to compat change "
- + r.options.getRequireCompatChangeId());
- return true;
+ + r.options.getRequireCompatChangeId();
}
if (!mService.validateAssociationAllowedLocked(r.callerPackage, r.callingUid,
filter.packageName, filter.owningUid)) {
- Slog.w(TAG, "Association not allowed: broadcasting "
+ return "Association not allowed: broadcasting "
+ r.intent.toString()
+ " from " + r.callerPackage + " (pid=" + r.callingPid
+ ", uid=" + r.callingUid + ") to " + filter.packageName + " through "
- + filter);
- return true;
+ + filter;
}
if (!mService.mIntentFirewall.checkBroadcast(r.intent, r.callingUid,
r.callingPid, r.resolvedType, filter.receiverList.uid)) {
- Slog.w(TAG, "Firewall blocked: broadcasting "
+ return "Firewall blocked: broadcasting "
+ r.intent.toString()
+ " from " + r.callerPackage + " (pid=" + r.callingPid
+ ", uid=" + r.callingUid + ") to " + filter.packageName + " through "
- + filter);
- return true;
+ + filter;
}
// Check that the sender has permission to send to this receiver
if (filter.requiredPermission != null) {
int perm = checkComponentPermission(filter.requiredPermission,
r.callingPid, r.callingUid, -1, true);
if (perm != PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: broadcasting "
+ return "Permission Denial: broadcasting "
+ r.intent.toString()
+ " from " + r.callerPackage + " (pid="
+ r.callingPid + ", uid=" + r.callingUid + ")"
+ " requires " + filter.requiredPermission
- + " due to registered receiver " + filter);
- return true;
+ + " due to registered receiver " + filter;
} else {
final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
if (opCode != AppOpsManager.OP_NONE
&& mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
!= AppOpsManager.MODE_ALLOWED) {
- Slog.w(TAG, "Appop Denial: broadcasting "
+ return "Appop Denial: broadcasting "
+ r.intent.toString()
+ " from " + r.callerPackage + " (pid="
+ r.callingPid + ", uid=" + r.callingUid + ")"
+ " requires appop " + AppOpsManager.permissionToOp(
filter.requiredPermission)
- + " due to registered receiver " + filter);
- return true;
+ + " due to registered receiver " + filter;
}
}
}
if ((filter.receiverList.app == null || filter.receiverList.app.isKilled()
|| filter.receiverList.app.mErrorState.isCrashing())) {
- Slog.w(TAG, "Skipping deliver [" + r.queue.toString() + "] " + r
- + " to " + filter.receiverList + ": process gone or crashing");
- return true;
+ return "Skipping deliver [" + r.queue.toString() + "] " + r
+ + " to " + filter.receiverList + ": process gone or crashing";
}
// Ensure that broadcasts are only sent to other Instant Apps if they are marked as
@@ -405,28 +411,26 @@
if (!visibleToInstantApps && filter.instantApp
&& filter.receiverList.uid != r.callingUid) {
- Slog.w(TAG, "Instant App Denial: receiving "
+ return "Instant App Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")"
- + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS");
- return true;
+ + " not specifying FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS";
}
if (!filter.visibleToInstantApp && r.callerInstantApp
&& filter.receiverList.uid != r.callingUid) {
- Slog.w(TAG, "Instant App Denial: receiving "
+ return "Instant App Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " requires receiver be visible to instant apps"
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
// Check that the receiver has the required permission(s) to receive this broadcast.
@@ -436,15 +440,14 @@
int perm = checkComponentPermission(requiredPermission,
filter.receiverList.pid, filter.receiverList.uid, -1, true);
if (perm != PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: receiving "
+ return "Permission Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " requires " + requiredPermission
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
int appOp = AppOpsManager.permissionToOpCode(requiredPermission);
if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp
@@ -452,7 +455,7 @@
filter.receiverList.uid, filter.packageName, filter.featureId,
"Broadcast delivered to registered receiver " + filter.receiverId)
!= AppOpsManager.MODE_ALLOWED) {
- Slog.w(TAG, "Appop Denial: receiving "
+ return "Appop Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
@@ -460,8 +463,7 @@
+ " requires appop " + AppOpsManager.permissionToOp(
requiredPermission)
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
}
}
@@ -469,14 +471,13 @@
int perm = checkComponentPermission(null,
filter.receiverList.pid, filter.receiverList.uid, -1, true);
if (perm != PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: security check failed when receiving "
+ return "Permission Denial: security check failed when receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
}
// Check that the receiver does *not* have any excluded permissions
@@ -496,7 +497,7 @@
filter.receiverList.uid,
filter.packageName)
== AppOpsManager.MODE_ALLOWED)) {
- Slog.w(TAG, "Appop Denial: receiving "
+ return "Appop Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
@@ -504,22 +505,20 @@
+ " excludes appop " + AppOpsManager.permissionToOp(
excludedPermission)
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
} else {
// When there is no app op associated with the permission,
// skip when permission is granted.
if (perm == PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Permission Denial: receiving "
+ return "Permission Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " excludes " + excludedPermission
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
}
}
@@ -528,15 +527,14 @@
// Check that the receiver does *not* belong to any of the excluded packages
if (r.excludedPackages != null && r.excludedPackages.length > 0) {
if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) {
- Slog.w(TAG, "Skipping delivery of excluded package "
+ return "Skipping delivery of excluded package "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " excludes package " + filter.packageName
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
}
@@ -546,15 +544,14 @@
filter.receiverList.uid, filter.packageName, filter.featureId,
"Broadcast delivered to registered receiver " + filter.receiverId)
!= AppOpsManager.MODE_ALLOWED) {
- Slog.w(TAG, "Appop Denial: receiving "
+ return "Appop Denial: receiving "
+ r.intent.toString()
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
+ " requires appop " + AppOpsManager.opToName(r.appOp)
+ " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")");
- return true;
+ + " (uid " + r.callingUid + ")";
}
// Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -562,15 +559,14 @@
if (!filter.exported && checkComponentPermission(null, r.callingPid,
r.callingUid, filter.receiverList.uid, filter.exported)
!= PackageManager.PERMISSION_GRANTED) {
- Slog.w(TAG, "Exported Denial: sending "
+ return "Exported Denial: sending "
+ r.intent.toString()
+ ", action: " + r.intent.getAction()
+ " from " + r.callerPackage
+ " (uid=" + r.callingUid + ")"
+ " due to receiver " + filter.receiverList.app
+ " (uid " + filter.receiverList.uid + ")"
- + " not specifying RECEIVER_EXPORTED");
- return true;
+ + " not specifying RECEIVER_EXPORTED";
}
// If permissions need a review before any of the app components can run, we drop
@@ -579,10 +575,10 @@
// broadcast.
if (!requestStartTargetPermissionsReviewIfNeededLocked(r, filter.packageName,
filter.owningUserId)) {
- return true;
+ return "Skipping delivery to " + filter.packageName + " due to permissions review";
}
- return false;
+ return null;
}
private static String broadcastDescription(BroadcastRecord r, ComponentName component) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 1225d99..64a8677 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3925,8 +3925,7 @@
@EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@Override
- public void showInputMethodPickerFromSystem(IInputMethodClient client, int auxiliarySubtypeMode,
- int displayId) {
+ public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) {
// Always call subtype picker, because subtype picker is a superset of input method
// picker.
mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId)
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index baa471c..bb37e0e 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -55,6 +55,7 @@
import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
import android.content.pm.overlay.OverlayPaths;
import android.content.res.ApkAssets;
import android.net.Uri;
@@ -887,7 +888,7 @@
}
}
- private Set<PackageAndUser> executeRequest(
+ private Set<UserPackage> executeRequest(
@NonNull final OverlayManagerTransaction.Request request)
throws OperationFailedException {
Objects.requireNonNull(request, "Transaction contains a null request");
@@ -932,7 +933,7 @@
try {
switch (request.type) {
case TYPE_SET_ENABLED:
- Set<PackageAndUser> result = null;
+ Set<UserPackage> result = null;
result = CollectionUtils.addAll(result,
mImpl.setEnabled(request.overlay, true, realUserId));
result = CollectionUtils.addAll(result,
@@ -973,7 +974,7 @@
synchronized (mLock) {
// execute the requests (as calling user)
- Set<PackageAndUser> affectedPackagesToUpdate = null;
+ Set<UserPackage> affectedPackagesToUpdate = null;
for (final OverlayManagerTransaction.Request request : transaction) {
affectedPackagesToUpdate = CollectionUtils.addAll(affectedPackagesToUpdate,
executeRequest(request));
@@ -1370,13 +1371,13 @@
}
}
- private void updateTargetPackagesLocked(@Nullable PackageAndUser updatedTarget) {
+ private void updateTargetPackagesLocked(@Nullable UserPackage updatedTarget) {
if (updatedTarget != null) {
updateTargetPackagesLocked(Set.of(updatedTarget));
}
}
- private void updateTargetPackagesLocked(@Nullable Set<PackageAndUser> updatedTargets) {
+ private void updateTargetPackagesLocked(@Nullable Set<UserPackage> updatedTargets) {
if (CollectionUtils.isEmpty(updatedTargets)) {
return;
}
@@ -1405,7 +1406,7 @@
@Nullable
private static SparseArray<ArraySet<String>> groupTargetsByUserId(
- @Nullable final Set<PackageAndUser> targetsAndUsers) {
+ @Nullable final Set<UserPackage> targetsAndUsers) {
final SparseArray<ArraySet<String>> userTargets = new SparseArray<>();
CollectionUtils.forEach(targetsAndUsers, target -> {
ArraySet<String> targets = userTargets.get(target.userId);
@@ -1472,7 +1473,7 @@
@NonNull
private SparseArray<List<String>> updatePackageManagerLocked(
- @Nullable Set<PackageAndUser> targets) {
+ @Nullable Set<UserPackage> targets) {
if (CollectionUtils.isEmpty(targets)) {
return new SparseArray<>();
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 17bb39c..6ffe60d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -35,6 +35,7 @@
import android.content.om.CriticalOverlayInfo;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
+import android.content.pm.UserPackage;
import android.content.pm.overlay.OverlayPaths;
import android.content.pm.parsing.FrameworkParsingPackageUtils;
import android.os.FabricatedOverlayInfo;
@@ -154,13 +155,13 @@
* set of targets that had, but no longer have, active overlays.
*/
@NonNull
- ArraySet<PackageAndUser> updateOverlaysForUser(final int newUserId) {
+ ArraySet<UserPackage> updateOverlaysForUser(final int newUserId) {
if (DEBUG) {
Slog.d(TAG, "updateOverlaysForUser newUserId=" + newUserId);
}
// Remove the settings of all overlays that are no longer installed for this user.
- final ArraySet<PackageAndUser> updatedTargets = new ArraySet<>();
+ final ArraySet<UserPackage> updatedTargets = new ArraySet<>();
final ArrayMap<String, AndroidPackage> userPackages = mPackageManager.initializeForUser(
newUserId);
CollectionUtils.addAll(updatedTargets, removeOverlaysForUser(
@@ -185,7 +186,7 @@
// When a new user is switched to for the first time, package manager must be
// informed of the overlay paths for all overlaid packages installed in the user.
if (overlaidByOthers.contains(pkg.getPackageName())) {
- updatedTargets.add(new PackageAndUser(pkg.getPackageName(), newUserId));
+ updatedTargets.add(UserPackage.of(newUserId, pkg.getPackageName()));
}
} catch (OperationFailedException e) {
Slog.e(TAG, "failed to initialize overlays of '" + pkg.getPackageName()
@@ -237,7 +238,7 @@
mSettings.setEnabled(overlay, newUserId, true);
if (updateState(oi, newUserId, 0)) {
CollectionUtils.add(updatedTargets,
- new PackageAndUser(oi.targetPackageName, oi.userId));
+ UserPackage.of(oi.userId, oi.targetPackageName));
}
}
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -258,40 +259,40 @@
}
@NonNull
- Set<PackageAndUser> onPackageAdded(@NonNull final String pkgName,
+ Set<UserPackage> onPackageAdded(@NonNull final String pkgName,
final int userId) throws OperationFailedException {
- final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+ final Set<UserPackage> updatedTargets = new ArraySet<>();
// Always update the overlays of newly added packages.
- updatedTargets.add(new PackageAndUser(pkgName, userId));
+ updatedTargets.add(UserPackage.of(userId, pkgName));
updatedTargets.addAll(reconcileSettingsForPackage(pkgName, userId, 0 /* flags */));
return updatedTargets;
}
@NonNull
- Set<PackageAndUser> onPackageChanged(@NonNull final String pkgName,
+ Set<UserPackage> onPackageChanged(@NonNull final String pkgName,
final int userId) throws OperationFailedException {
return reconcileSettingsForPackage(pkgName, userId, 0 /* flags */);
}
@NonNull
- Set<PackageAndUser> onPackageReplacing(@NonNull final String pkgName, final int userId)
+ Set<UserPackage> onPackageReplacing(@NonNull final String pkgName, final int userId)
throws OperationFailedException {
return reconcileSettingsForPackage(pkgName, userId, FLAG_OVERLAY_IS_BEING_REPLACED);
}
@NonNull
- Set<PackageAndUser> onPackageReplaced(@NonNull final String pkgName, final int userId)
+ Set<UserPackage> onPackageReplaced(@NonNull final String pkgName, final int userId)
throws OperationFailedException {
return reconcileSettingsForPackage(pkgName, userId, 0 /* flags */);
}
@NonNull
- Set<PackageAndUser> onPackageRemoved(@NonNull final String pkgName, final int userId) {
+ Set<UserPackage> onPackageRemoved(@NonNull final String pkgName, final int userId) {
if (DEBUG) {
Slog.d(TAG, "onPackageRemoved pkgName=" + pkgName + " userId=" + userId);
}
// Update the state of all overlays that target this package.
- final Set<PackageAndUser> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
+ final Set<UserPackage> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
// Remove all the overlays this package declares.
return CollectionUtils.addAll(targets,
@@ -299,15 +300,15 @@
}
@NonNull
- private Set<PackageAndUser> removeOverlaysForUser(
+ private Set<UserPackage> removeOverlaysForUser(
@NonNull final Predicate<OverlayInfo> condition, final int userId) {
final List<OverlayInfo> overlays = mSettings.removeIf(
io -> userId == io.userId && condition.test(io));
- Set<PackageAndUser> targets = Collections.emptySet();
+ Set<UserPackage> targets = Collections.emptySet();
for (int i = 0, n = overlays.size(); i < n; i++) {
final OverlayInfo info = overlays.get(i);
targets = CollectionUtils.add(targets,
- new PackageAndUser(info.targetPackageName, userId));
+ UserPackage.of(userId, info.targetPackageName));
// Remove the idmap if the overlay is no longer installed for any user.
removeIdmapIfPossible(info);
@@ -316,7 +317,7 @@
}
@NonNull
- private Set<PackageAndUser> updateOverlaysForTarget(@NonNull final String targetPackage,
+ private Set<UserPackage> updateOverlaysForTarget(@NonNull final String targetPackage,
final int userId, final int flags) {
boolean modified = false;
final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackage, userId);
@@ -332,18 +333,18 @@
if (!modified) {
return Collections.emptySet();
}
- return Set.of(new PackageAndUser(targetPackage, userId));
+ return Set.of(UserPackage.of(userId, targetPackage));
}
@NonNull
- private Set<PackageAndUser> updatePackageOverlays(@NonNull AndroidPackage pkg,
+ private Set<UserPackage> updatePackageOverlays(@NonNull AndroidPackage pkg,
final int userId, final int flags) throws OperationFailedException {
if (pkg.getOverlayTarget() == null) {
// This package does not have overlays declared in its manifest.
return Collections.emptySet();
}
- Set<PackageAndUser> updatedTargets = Collections.emptySet();
+ Set<UserPackage> updatedTargets = Collections.emptySet();
final OverlayIdentifier overlay = new OverlayIdentifier(pkg.getPackageName());
final int priority = getPackageConfiguredPriority(pkg);
try {
@@ -353,7 +354,7 @@
// If the targetPackageName has changed, the package that *used* to
// be the target must also update its assets.
updatedTargets = CollectionUtils.add(updatedTargets,
- new PackageAndUser(currentInfo.targetPackageName, userId));
+ UserPackage.of(userId, currentInfo.targetPackageName));
}
currentInfo = mSettings.init(overlay, userId, pkg.getOverlayTarget(),
@@ -367,13 +368,13 @@
// reinitialized. Reorder the overlay and update its target package.
mSettings.setPriority(overlay, userId, priority);
updatedTargets = CollectionUtils.add(updatedTargets,
- new PackageAndUser(currentInfo.targetPackageName, userId));
+ UserPackage.of(userId, currentInfo.targetPackageName));
}
// Update the enabled state of the overlay.
if (updateState(currentInfo, userId, flags)) {
updatedTargets = CollectionUtils.add(updatedTargets,
- new PackageAndUser(currentInfo.targetPackageName, userId));
+ UserPackage.of(userId, currentInfo.targetPackageName));
}
} catch (OverlayManagerSettings.BadKeyException e) {
throw new OperationFailedException("failed to update settings", e);
@@ -382,14 +383,14 @@
}
@NonNull
- private Set<PackageAndUser> reconcileSettingsForPackage(@NonNull final String pkgName,
+ private Set<UserPackage> reconcileSettingsForPackage(@NonNull final String pkgName,
final int userId, final int flags) throws OperationFailedException {
if (DEBUG) {
Slog.d(TAG, "reconcileSettingsForPackage pkgName=" + pkgName + " userId=" + userId);
}
// Update the state of overlays that target this package.
- Set<PackageAndUser> updatedTargets = Collections.emptySet();
+ Set<UserPackage> updatedTargets = Collections.emptySet();
updatedTargets = CollectionUtils.addAll(updatedTargets,
updateOverlaysForTarget(pkgName, userId, flags));
@@ -423,7 +424,7 @@
}
@NonNull
- Set<PackageAndUser> setEnabled(@NonNull final OverlayIdentifier overlay,
+ Set<UserPackage> setEnabled(@NonNull final OverlayIdentifier overlay,
final boolean enable, final int userId) throws OperationFailedException {
if (DEBUG) {
Slog.d(TAG, String.format("setEnabled overlay=%s enable=%s userId=%d",
@@ -442,7 +443,7 @@
modified |= updateState(oi, userId, 0);
if (modified) {
- return Set.of(new PackageAndUser(oi.targetPackageName, userId));
+ return Set.of(UserPackage.of(userId, oi.targetPackageName));
}
return Set.of();
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -450,7 +451,7 @@
}
}
- Optional<PackageAndUser> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
+ Optional<UserPackage> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
boolean withinCategory, final int userId) throws OperationFailedException {
if (DEBUG) {
Slog.d(TAG, String.format("setEnabledExclusive overlay=%s"
@@ -493,7 +494,7 @@
modified |= updateState(enabledInfo, userId, 0);
if (modified) {
- return Optional.of(new PackageAndUser(enabledInfo.targetPackageName, userId));
+ return Optional.of(UserPackage.of(userId, enabledInfo.targetPackageName));
}
return Optional.empty();
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -502,7 +503,7 @@
}
@NonNull
- Set<PackageAndUser> registerFabricatedOverlay(
+ Set<UserPackage> registerFabricatedOverlay(
@NonNull final FabricatedOverlayInternal overlay)
throws OperationFailedException {
if (FrameworkParsingPackageUtils.validateName(overlay.overlayName,
@@ -516,7 +517,7 @@
throw new OperationFailedException("failed to create fabricated overlay");
}
- final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+ final Set<UserPackage> updatedTargets = new ArraySet<>();
for (int userId : mSettings.getUsers()) {
updatedTargets.addAll(registerFabricatedOverlay(info, userId));
}
@@ -524,13 +525,13 @@
}
@NonNull
- private Set<PackageAndUser> registerFabricatedOverlay(
+ private Set<UserPackage> registerFabricatedOverlay(
@NonNull final FabricatedOverlayInfo info, int userId)
throws OperationFailedException {
final OverlayIdentifier overlayIdentifier = new OverlayIdentifier(
info.packageName, info.overlayName);
- final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+ final Set<UserPackage> updatedTargets = new ArraySet<>();
OverlayInfo oi = mSettings.getNullableOverlayInfo(overlayIdentifier, userId);
if (oi != null) {
if (!oi.isFabricated) {
@@ -543,7 +544,7 @@
if (oi != null) {
// If the fabricated overlay changes its target package, update the previous
// target package so it no longer is overlaid.
- updatedTargets.add(new PackageAndUser(oi.targetPackageName, userId));
+ updatedTargets.add(UserPackage.of(userId, oi.targetPackageName));
}
oi = mSettings.init(overlayIdentifier, userId, info.targetPackageName,
info.targetOverlayable, info.path, true, false,
@@ -554,7 +555,7 @@
mSettings.setBaseCodePath(overlayIdentifier, userId, info.path);
}
if (updateState(oi, userId, 0)) {
- updatedTargets.add(new PackageAndUser(oi.targetPackageName, userId));
+ updatedTargets.add(UserPackage.of(userId, oi.targetPackageName));
}
} catch (OverlayManagerSettings.BadKeyException e) {
throw new OperationFailedException("failed to update settings", e);
@@ -564,8 +565,8 @@
}
@NonNull
- Set<PackageAndUser> unregisterFabricatedOverlay(@NonNull final OverlayIdentifier overlay) {
- final Set<PackageAndUser> updatedTargets = new ArraySet<>();
+ Set<UserPackage> unregisterFabricatedOverlay(@NonNull final OverlayIdentifier overlay) {
+ final Set<UserPackage> updatedTargets = new ArraySet<>();
for (int userId : mSettings.getUsers()) {
updatedTargets.addAll(unregisterFabricatedOverlay(overlay, userId));
}
@@ -573,7 +574,7 @@
}
@NonNull
- private Set<PackageAndUser> unregisterFabricatedOverlay(
+ private Set<UserPackage> unregisterFabricatedOverlay(
@NonNull final OverlayIdentifier overlay, int userId) {
final OverlayInfo oi = mSettings.getNullableOverlayInfo(overlay, userId);
if (oi != null) {
@@ -581,7 +582,7 @@
if (oi.isEnabled()) {
// Removing a fabricated overlay only changes the overlay path of a package if it is
// currently enabled.
- return Set.of(new PackageAndUser(oi.targetPackageName, userId));
+ return Set.of(UserPackage.of(userId, oi.targetPackageName));
}
}
return Set.of();
@@ -627,7 +628,7 @@
return mOverlayConfig.isEnabled(overlay.getPackageName());
}
- Optional<PackageAndUser> setPriority(@NonNull final OverlayIdentifier overlay,
+ Optional<UserPackage> setPriority(@NonNull final OverlayIdentifier overlay,
@NonNull final OverlayIdentifier newParentOverlay, final int userId)
throws OperationFailedException {
try {
@@ -644,7 +645,7 @@
}
if (mSettings.setPriority(overlay, newParentOverlay, userId)) {
- return Optional.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+ return Optional.of(UserPackage.of(userId, overlayInfo.targetPackageName));
}
return Optional.empty();
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -652,7 +653,7 @@
}
}
- Set<PackageAndUser> setHighestPriority(@NonNull final OverlayIdentifier overlay,
+ Set<UserPackage> setHighestPriority(@NonNull final OverlayIdentifier overlay,
final int userId) throws OperationFailedException {
try{
if (DEBUG) {
@@ -667,7 +668,7 @@
}
if (mSettings.setHighestPriority(overlay, userId)) {
- return Set.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+ return Set.of(UserPackage.of(userId, overlayInfo.targetPackageName));
}
return Set.of();
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -675,7 +676,7 @@
}
}
- Optional<PackageAndUser> setLowestPriority(@NonNull final OverlayIdentifier overlay,
+ Optional<UserPackage> setLowestPriority(@NonNull final OverlayIdentifier overlay,
final int userId) throws OperationFailedException {
try{
if (DEBUG) {
@@ -690,7 +691,7 @@
}
if (mSettings.setLowestPriority(overlay, userId)) {
- return Optional.of(new PackageAndUser(overlayInfo.targetPackageName, userId));
+ return Optional.of(UserPackage.of(userId, overlayInfo.targetPackageName));
}
return Optional.empty();
} catch (OverlayManagerSettings.BadKeyException e) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index b979e7a..b302d4a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -107,6 +107,7 @@
import android.content.pm.SuspendDialogInfo;
import android.content.pm.TestUtilityService;
import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
import android.content.pm.VerifierDeviceIdentity;
import android.content.pm.VersionedPackage;
import android.content.pm.overlay.OverlayPaths;
@@ -2986,6 +2987,7 @@
@Override
public void notifyPackageRemoved(String packageName, int uid) {
mPackageObserverHelper.notifyRemoved(packageName, uid);
+ UserPackage.removeFromCache(UserHandle.getUserId(uid), packageName);
}
void sendPackageAddedForUser(@NonNull Computer snapshot, String packageName,
diff --git a/services/core/java/com/android/server/pm/ShortcutLauncher.java b/services/core/java/com/android/server/pm/ShortcutLauncher.java
index 0d99075..00f7dc4 100644
--- a/services/core/java/com/android/server/pm/ShortcutLauncher.java
+++ b/services/core/java/com/android/server/pm/ShortcutLauncher.java
@@ -20,6 +20,7 @@
import android.annotation.UserIdInt;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
+import android.content.pm.UserPackage;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
@@ -30,7 +31,6 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.pm.ShortcutService.DumpFilter;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
import libcore.io.IoUtils;
@@ -70,7 +70,7 @@
/**
* Package name -> IDs.
*/
- final private ArrayMap<PackageWithUser, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
+ private final ArrayMap<UserPackage, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
private ShortcutLauncher(@NonNull ShortcutUser shortcutUser,
@UserIdInt int ownerUserId, @NonNull String packageName,
@@ -101,12 +101,12 @@
* Called when the new package can't receive the backup, due to signature or version mismatch.
*/
private void onRestoreBlocked() {
- final ArrayList<PackageWithUser> pinnedPackages =
+ final ArrayList<UserPackage> pinnedPackages =
new ArrayList<>(mPinnedShortcuts.keySet());
mPinnedShortcuts.clear();
for (int i = pinnedPackages.size() - 1; i >= 0; i--) {
- final PackageWithUser pu = pinnedPackages.get(i);
- final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(pu.packageName);
+ final UserPackage up = pinnedPackages.get(i);
+ final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(up.packageName);
if (p != null) {
p.refreshPinnedFlags();
}
@@ -135,13 +135,13 @@
return; // No need to instantiate.
}
- final PackageWithUser pu = PackageWithUser.of(packageUserId, packageName);
+ final UserPackage up = UserPackage.of(packageUserId, packageName);
final int idSize = ids.size();
if (idSize == 0) {
- mPinnedShortcuts.remove(pu);
+ mPinnedShortcuts.remove(up);
} else {
- final ArraySet<String> prevSet = mPinnedShortcuts.get(pu);
+ final ArraySet<String> prevSet = mPinnedShortcuts.get(up);
// Actually pin shortcuts.
// This logic here is to make sure a launcher cannot pin a shortcut that is not dynamic
@@ -165,7 +165,7 @@
newSet.add(id);
}
}
- mPinnedShortcuts.put(pu, newSet);
+ mPinnedShortcuts.put(up, newSet);
}
packageShortcuts.refreshPinnedFlags();
}
@@ -176,7 +176,7 @@
@Nullable
public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName,
@UserIdInt int packageUserId) {
- return mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName));
+ return mPinnedShortcuts.get(UserPackage.of(packageUserId, packageName));
}
/**
@@ -207,7 +207,7 @@
}
boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) {
- return mPinnedShortcuts.remove(PackageWithUser.of(packageUserId, packageName)) != null;
+ return mPinnedShortcuts.remove(UserPackage.of(packageUserId, packageName)) != null;
}
public void ensurePackageInfo() {
@@ -241,15 +241,15 @@
getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
for (int i = 0; i < size; i++) {
- final PackageWithUser pu = mPinnedShortcuts.keyAt(i);
+ final UserPackage up = mPinnedShortcuts.keyAt(i);
- if (forBackup && (pu.userId != getOwnerUserId())) {
+ if (forBackup && (up.userId != getOwnerUserId())) {
continue; // Target package on a different user, skip. (i.e. work profile)
}
out.startTag(null, TAG_PACKAGE);
- ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, pu.packageName);
- ShortcutService.writeAttr(out, ATTR_PACKAGE_USER_ID, pu.userId);
+ ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, up.packageName);
+ ShortcutService.writeAttr(out, ATTR_PACKAGE_USER_ID, up.userId);
final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
final int idSize = ids.size();
@@ -345,7 +345,7 @@
ATTR_PACKAGE_USER_ID, ownerUserId);
ids = new ArraySet<>();
ret.mPinnedShortcuts.put(
- PackageWithUser.of(packageUserId, packageName), ids);
+ UserPackage.of(packageUserId, packageName), ids);
continue;
}
}
@@ -386,14 +386,14 @@
for (int i = 0; i < size; i++) {
pw.println();
- final PackageWithUser pu = mPinnedShortcuts.keyAt(i);
+ final UserPackage up = mPinnedShortcuts.keyAt(i);
pw.print(prefix);
pw.print(" ");
pw.print("Package: ");
- pw.print(pu.packageName);
+ pw.print(up.packageName);
pw.print(" User: ");
- pw.println(pu.userId);
+ pw.println(up.userId);
final ArraySet<String> ids = mPinnedShortcuts.valueAt(i);
final int idSize = ids.size();
@@ -418,7 +418,7 @@
@VisibleForTesting
ArraySet<String> getAllPinnedShortcutsForTest(String packageName, int packageUserId) {
- return new ArraySet<>(mPinnedShortcuts.get(PackageWithUser.of(packageUserId, packageName)));
+ return new ArraySet<>(mPinnedShortcuts.get(UserPackage.of(packageUserId, packageName)));
}
@Override
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index b6f09ff..83720f1 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -57,6 +57,7 @@
import android.content.pm.ShortcutManager;
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
+import android.content.pm.UserPackage;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Bitmap;
@@ -118,7 +119,6 @@
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
import com.android.server.SystemService;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
import com.android.server.uri.UriGrantsManagerInternal;
import libcore.io.IoUtils;
@@ -3774,7 +3774,7 @@
final long start = getStatStartTime();
try {
- final ArrayList<PackageWithUser> gonePackages = new ArrayList<>();
+ final ArrayList<UserPackage> gonePackages = new ArrayList<>();
synchronized (mLock) {
final ShortcutUser user = getUserShortcutsLocked(ownerUserId);
@@ -3789,13 +3789,14 @@
Slog.d(TAG, "Uninstalled: " + spi.getPackageName()
+ " user " + spi.getPackageUserId());
}
- gonePackages.add(PackageWithUser.of(spi));
+ gonePackages.add(
+ UserPackage.of(spi.getPackageUserId(), spi.getPackageName()));
}
});
if (gonePackages.size() > 0) {
for (int i = gonePackages.size() - 1; i >= 0; i--) {
- final PackageWithUser pu = gonePackages.get(i);
- cleanUpPackageLocked(pu.packageName, ownerUserId, pu.userId,
+ final UserPackage up = gonePackages.get(i);
+ cleanUpPackageLocked(up.packageName, ownerUserId, up.userId,
/* appStillExists = */ false);
}
}
@@ -5274,7 +5275,7 @@
final ShortcutUser user = mUsers.get(userId);
if (user == null) return null;
- return user.getAllLaunchersForTest().get(PackageWithUser.of(userId, packageName));
+ return user.getAllLaunchersForTest().get(UserPackage.of(userId, packageName));
}
}
diff --git a/services/core/java/com/android/server/pm/ShortcutUser.java b/services/core/java/com/android/server/pm/ShortcutUser.java
index 20bbf46..94eb6bb 100644
--- a/services/core/java/com/android/server/pm/ShortcutUser.java
+++ b/services/core/java/com/android/server/pm/ShortcutUser.java
@@ -21,6 +21,7 @@
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchSession;
import android.content.pm.ShortcutManager;
+import android.content.pm.UserPackage;
import android.metrics.LogMaker;
import android.os.Binder;
import android.os.FileUtils;
@@ -52,7 +53,6 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -82,44 +82,6 @@
private static final String KEY_LAUNCHERS = "launchers";
private static final String KEY_PACKAGES = "packages";
- static final class PackageWithUser {
- final int userId;
- final String packageName;
-
- private PackageWithUser(int userId, String packageName) {
- this.userId = userId;
- this.packageName = Objects.requireNonNull(packageName);
- }
-
- public static PackageWithUser of(int userId, String packageName) {
- return new PackageWithUser(userId, packageName);
- }
-
- public static PackageWithUser of(ShortcutPackageItem spi) {
- return new PackageWithUser(spi.getPackageUserId(), spi.getPackageName());
- }
-
- @Override
- public int hashCode() {
- return packageName.hashCode() ^ userId;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof PackageWithUser)) {
- return false;
- }
- final PackageWithUser that = (PackageWithUser) obj;
-
- return userId == that.userId && packageName.equals(that.packageName);
- }
-
- @Override
- public String toString() {
- return String.format("[Package: %d, %s]", userId, packageName);
- }
- }
-
final ShortcutService mService;
final AppSearchManager mAppSearchManager;
final Executor mExecutor;
@@ -129,7 +91,7 @@
private final ArrayMap<String, ShortcutPackage> mPackages = new ArrayMap<>();
- private final ArrayMap<PackageWithUser, ShortcutLauncher> mLaunchers = new ArrayMap<>();
+ private final ArrayMap<UserPackage, ShortcutLauncher> mLaunchers = new ArrayMap<>();
/** In-memory-cached default launcher. */
private String mCachedLauncher;
@@ -204,20 +166,20 @@
// We don't expose this directly to non-test code because only ShortcutUser should add to/
// remove from it.
@VisibleForTesting
- ArrayMap<PackageWithUser, ShortcutLauncher> getAllLaunchersForTest() {
+ ArrayMap<UserPackage, ShortcutLauncher> getAllLaunchersForTest() {
return mLaunchers;
}
private void addLauncher(ShortcutLauncher launcher) {
launcher.replaceUser(this);
- mLaunchers.put(PackageWithUser.of(launcher.getPackageUserId(),
+ mLaunchers.put(UserPackage.of(launcher.getPackageUserId(),
launcher.getPackageName()), launcher);
}
@Nullable
public ShortcutLauncher removeLauncher(
@UserIdInt int packageUserId, @NonNull String packageName) {
- return mLaunchers.remove(PackageWithUser.of(packageUserId, packageName));
+ return mLaunchers.remove(UserPackage.of(packageUserId, packageName));
}
@Nullable
@@ -242,7 +204,7 @@
@NonNull
public ShortcutLauncher getLauncherShortcuts(@NonNull String packageName,
@UserIdInt int launcherUserId) {
- final PackageWithUser key = PackageWithUser.of(launcherUserId, packageName);
+ final UserPackage key = UserPackage.of(launcherUserId, packageName);
ShortcutLauncher ret = mLaunchers.get(key);
if (ret == null) {
ret = new ShortcutLauncher(this, mUserId, packageName, launcherUserId);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index cf0ea43..d2af78b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -52,6 +52,7 @@
import android.content.pm.ShortcutServiceInternal;
import android.content.pm.UserInfo;
import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserPackage;
import android.content.pm.UserProperties;
import android.content.pm.parsing.FrameworkParsingPackageUtils;
import android.content.res.Configuration;
@@ -5848,6 +5849,7 @@
Slog.d(LOG_TAG, "updateUserIds(): userIds= " + Arrays.toString(mUserIds)
+ " includingPreCreated=" + Arrays.toString(mUserIdsIncludingPreCreated));
}
+ UserPackage.setValidUserIds(mUserIds);
}
}
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 80c9803..a64bd69 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -469,6 +469,48 @@
}
/**
+ * Records that a particular container has been reparented. This only effects windows that have
+ * already been collected in the transition. This should be called before reparenting because
+ * the old parent may be removed during reparenting, for example:
+ * {@link Task#shouldRemoveSelfOnLastChildRemoval}
+ */
+ void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) {
+ if (!mChanges.containsKey(wc)) {
+ // #collectReparentChange() will be called when the window is reparented. Skip if it is
+ // a window that has not been collected, which means we don't care about this window for
+ // the current transition.
+ return;
+ }
+ final ChangeInfo change = mChanges.get(wc);
+ // Use the current common ancestor if there are multiple reparent, and the original parent
+ // has been detached. Otherwise, use the original parent before the transition.
+ final WindowContainer prevParent =
+ change.mStartParent == null || change.mStartParent.isAttached()
+ ? change.mStartParent
+ : change.mCommonAncestor;
+ if (prevParent == null || !prevParent.isAttached()) {
+ Slog.w(TAG, "Trying to collect reparenting of a window after the previous parent has"
+ + " been detached: " + wc);
+ return;
+ }
+ if (prevParent == newParent) {
+ Slog.w(TAG, "Trying to collect reparenting of a window that has not been reparented: "
+ + wc);
+ return;
+ }
+ if (!newParent.isAttached()) {
+ Slog.w(TAG, "Trying to collect reparenting of a window that is not attached after"
+ + " reparenting: " + wc);
+ return;
+ }
+ WindowContainer ancestor = newParent;
+ while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
+ ancestor = ancestor.getParent();
+ }
+ change.mCommonAncestor = ancestor;
+ }
+
+ /**
* @return {@code true} if `wc` is a participant or is a descendant of one.
*/
boolean isInTransition(WindowContainer wc) {
@@ -830,8 +872,8 @@
void abort() {
// This calls back into itself via controller.abort, so just early return here.
if (mState == STATE_ABORT) return;
- if (mState != STATE_COLLECTING) {
- throw new IllegalStateException("Too late to abort.");
+ if (mState != STATE_COLLECTING && mState != STATE_STARTED) {
+ throw new IllegalStateException("Too late to abort. state=" + mState);
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Aborting Transition: %d", mSyncId);
mState = STATE_ABORT;
@@ -1524,20 +1566,7 @@
return out;
}
- // Find the top-most shared ancestor of app targets.
- WindowContainer<?> ancestor = topApp.getParent();
- // Go up ancestor parent chain until all targets are descendants.
- ancestorLoop:
- while (ancestor != null) {
- for (int i = sortedTargets.size() - 1; i >= 0; --i) {
- final WindowContainer wc = sortedTargets.get(i);
- if (!isWallpaper(wc) && !wc.isDescendantOf(ancestor)) {
- ancestor = ancestor.getParent();
- continue ancestorLoop;
- }
- }
- break;
- }
+ WindowContainer<?> ancestor = findCommonAncestor(sortedTargets, changes, topApp);
// make leash based on highest (z-order) direct child of ancestor with a participant.
WindowContainer leashReference = sortedTargets.get(0);
@@ -1654,6 +1683,46 @@
return out;
}
+ /**
+ * Finds the top-most common ancestor of app targets.
+ *
+ * Makes sure that the previous parent is also a descendant to make sure the animation won't
+ * be covered by other windows below the previous parent. For example, when reparenting an
+ * activity from PiP Task to split screen Task.
+ */
+ @NonNull
+ private static WindowContainer<?> findCommonAncestor(
+ @NonNull ArrayList<WindowContainer> targets,
+ @NonNull ArrayMap<WindowContainer, ChangeInfo> changes,
+ @NonNull WindowContainer<?> topApp) {
+ WindowContainer<?> ancestor = topApp.getParent();
+ // Go up ancestor parent chain until all targets are descendants. Ancestor should never be
+ // null because all targets are attached.
+ for (int i = targets.size() - 1; i >= 0; i--) {
+ final WindowContainer wc = targets.get(i);
+ if (isWallpaper(wc)) {
+ // Skip the non-app window.
+ continue;
+ }
+ while (!wc.isDescendantOf(ancestor)) {
+ ancestor = ancestor.getParent();
+ }
+
+ // Make sure the previous parent is also a descendant to make sure the animation won't
+ // be covered by other windows below the previous parent. For example, when reparenting
+ // an activity from PiP Task to split screen Task.
+ final ChangeInfo change = changes.get(wc);
+ final WindowContainer prevParent = change.mCommonAncestor;
+ if (prevParent == null || !prevParent.isAttached()) {
+ continue;
+ }
+ while (prevParent != ancestor && !prevParent.isDescendantOf(ancestor)) {
+ ancestor = ancestor.getParent();
+ }
+ }
+ return ancestor;
+ }
+
private static WindowManager.LayoutParams getLayoutParamsForAnimationsStyle(int type,
ArrayList<WindowContainer> sortedTargets) {
// Find the layout params of the top-most application window that is part of the
@@ -1772,10 +1841,19 @@
@Retention(RetentionPolicy.SOURCE)
@interface Flag {}
- // Usually "post" change state.
+ /**
+ * "Parent" that is also included in the transition. When populating the parent changes, we
+ * may skip the intermediate parents, so this may not be the actual parent in the hierarchy.
+ */
WindowContainer mEndParent;
- // Parent before change state.
+ /** Actual parent window before change state. */
WindowContainer mStartParent;
+ /**
+ * When the window is reparented during the transition, this is the common ancestor window
+ * of the {@link #mStartParent} and the current parent. This is needed because the
+ * {@link #mStartParent} may have been detached when the transition starts.
+ */
+ WindowContainer mCommonAncestor;
// State tracking
boolean mExistenceChanged = false;
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index ac85c9a..37bef3a 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -533,6 +533,17 @@
mCollectingTransition.collectVisibleChange(wc);
}
+ /**
+ * Records that a particular container has been reparented. This only effects windows that have
+ * already been collected in the transition. This should be called before reparenting because
+ * the old parent may be removed during reparenting, for example:
+ * {@link Task#shouldRemoveSelfOnLastChildRemoval}
+ */
+ void collectReparentChange(@NonNull WindowContainer wc, @NonNull WindowContainer newParent) {
+ if (!isCollecting()) return;
+ mCollectingTransition.collectReparentChange(wc, newParent);
+ }
+
/** @see Transition#mStatusBarTransitionDelay */
void setStatusBarTransitionDelay(long delay) {
if (mCollectingTransition == null) return;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 0b5de85..73d4496 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -542,6 +542,10 @@
throw new IllegalArgumentException("WC=" + this + " already child of " + mParent);
}
+ // Collect before removing child from old parent, because the old parent may be removed if
+ // this is the last child in it.
+ mTransitionController.collectReparentChange(this, newParent);
+
// The display object before reparenting as that might lead to old parent getting removed
// from the display if it no longer has any child.
final DisplayContent prevDc = oldParent.getDisplayContent();
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
index 5f25e3d..dcf094f 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java
@@ -83,8 +83,9 @@
*/
public void show(RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) {
Log.i(TAG, "In show");
- Intent intent = IntentFactory.newIntent(requestInfo, providerDataList,
- mResultReceiver);
+ Intent intent = IntentFactory.newIntent(
+ requestInfo, providerDataList,
+ new ArrayList<>(), mResultReceiver);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 24610df..ff2107a 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -20,7 +20,7 @@
import android.annotation.Nullable;
import android.app.slice.Slice;
import android.credentials.ui.Entry;
-import android.credentials.ui.ProviderData;
+import android.credentials.ui.GetCredentialProviderData;
import android.service.credentials.Action;
import android.service.credentials.CredentialEntry;
import android.service.credentials.CredentialProviderInfo;
@@ -117,7 +117,7 @@
}
@Override
- protected final ProviderData prepareUiData() throws IllegalArgumentException {
+ protected GetCredentialProviderData prepareUiData() throws IllegalArgumentException {
Log.i(TAG, "In prepareUiData");
if (!ProviderSession.isCompletionStatus(getStatus())) {
Log.i(TAG, "In prepareUiData not complete");
@@ -147,7 +147,7 @@
* To be called by {@link ProviderGetSession} when the UI is to be invoked.
*/
@Nullable
- private ProviderData prepareUiProviderDataWithCredentials(@NonNull
+ private GetCredentialProviderData prepareUiProviderDataWithCredentials(@NonNull
CredentialsDisplayContent content) {
Log.i(TAG, "in prepareUiProviderData");
List<Entry> credentialEntries = new ArrayList<>();
@@ -173,15 +173,10 @@
action.getSlice()));
}
- // TODO : Set the correct last used time
- return new ProviderData.Builder(mComponentName.flattenToString(),
- mProviderInfo.getServiceLabel() == null ? "" :
- mProviderInfo.getServiceLabel().toString(),
- /*icon=*/null)
+ return new GetCredentialProviderData.Builder(mComponentName.flattenToString())
.setCredentialEntries(credentialEntries)
.setActionChips(actionChips)
.setAuthenticationEntry(authenticationEntry)
- .setLastUsedTimeMillis(0)
.build();
}
@@ -189,7 +184,7 @@
* To be called by {@link ProviderGetSession} when the UI is to be invoked.
*/
@Nullable
- private ProviderData prepareUiProviderDataWithAuthentication(@NonNull
+ private GetCredentialProviderData prepareUiProviderDataWithAuthentication(@NonNull
Action authenticationEntry) {
// TODO : Implement authentication flow
return null;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index e1a4c1d..de59603 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -280,13 +280,13 @@
constants.TIMEOUT = 100;
constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
- public boolean shouldSkip(BroadcastRecord r, ResolveInfo info) {
+ public boolean shouldSkip(BroadcastRecord r, Object o) {
// Ignored
return false;
}
- public boolean shouldSkip(BroadcastRecord r, BroadcastFilter filter) {
+ public String shouldSkipMessage(BroadcastRecord r, Object o) {
// Ignored
- return false;
+ return null;
}
};
final BroadcastHistory emptyHistory = new BroadcastHistory(constants) {
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
index 3f55f1b..ec61b87 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
@@ -20,6 +20,7 @@
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
+import android.content.pm.UserPackage;
import androidx.test.runner.AndroidJUnit4;
@@ -54,7 +55,7 @@
addPackage(target(otherTarget), USER);
addPackage(overlay(OVERLAY, TARGET), USER);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
// The result should be the same for every time
assertThat(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
@@ -67,7 +68,7 @@
addPackage(target(TARGET), USER);
addPackage(overlay(OVERLAY, TARGET), USER);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_DISABLED, 0 /* priority */);
expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
@@ -101,7 +102,7 @@
addPackage(overlay(OVERLAY, TARGET), USER);
configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -133,7 +134,7 @@
addPackage(target(TARGET), USER);
addPackage(overlay(OVERLAY, TARGET), USER);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
final Consumer<ConfigState> setOverlay = (state -> {
configureSystemOverlay(OVERLAY, state, 0 /* priority */);
@@ -185,7 +186,7 @@
configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 0 /* priority */);
configureSystemOverlay(OVERLAY2, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -230,7 +231,7 @@
configureSystemOverlay(OVERLAY, ConfigState.IMMUTABLE_ENABLED, 0 /* priority */);
configureSystemOverlay(OVERLAY2, ConfigState.IMMUTABLE_ENABLED, 1 /* priority */);
- final Set<PackageAndUser> allPackages = Set.of(new PackageAndUser(TARGET, USER));
+ final Set<UserPackage> allPackages = Set.of(UserPackage.of(USER, TARGET));
expect.that(impl.updateOverlaysForUser(USER)).isEqualTo(allPackages);
final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
index f69141d..ab52928 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
@@ -22,7 +22,6 @@
import static android.os.OverlayablePolicy.CONFIG_SIGNATURE;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -30,7 +29,7 @@
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
-import android.util.Pair;
+import android.content.pm.UserPackage;
import androidx.test.runner.AndroidJUnit4;
@@ -66,7 +65,7 @@
@Test
public void testGetOverlayInfo() throws Exception {
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final OverlayManagerServiceImpl impl = getImpl();
final OverlayInfo oi = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -79,11 +78,11 @@
@Test
public void testGetOverlayInfosForTarget() throws Exception {
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY2, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY3, TARGET), USER2,
- Set.of(new PackageAndUser(OVERLAY3, USER2), new PackageAndUser(TARGET, USER2)));
+ Set.of(UserPackage.of(USER2, OVERLAY3), UserPackage.of(USER2, TARGET)));
final OverlayManagerServiceImpl impl = getImpl();
final List<OverlayInfo> ois = impl.getOverlayInfosForTarget(TARGET, USER);
@@ -107,13 +106,13 @@
@Test
public void testGetOverlayInfosForUser() throws Exception {
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY2, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY3, TARGET2), USER,
- Set.of(new PackageAndUser(OVERLAY3, USER), new PackageAndUser(TARGET2, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY3), UserPackage.of(USER, TARGET2)));
final OverlayManagerServiceImpl impl = getImpl();
final Map<String, List<OverlayInfo>> everything = impl.getOverlaysForUser(USER);
@@ -138,11 +137,11 @@
@Test
public void testPriority() throws Exception {
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY2, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY2, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY2), UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY3, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY3, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY3), UserPackage.of(USER, TARGET)));
final OverlayManagerServiceImpl impl = getImpl();
final OverlayInfo o1 = impl.getOverlayInfo(IDENTIFIER, USER);
@@ -152,15 +151,15 @@
assertOverlayInfoForTarget(TARGET, USER, o1, o2, o3);
assertEquals(impl.setLowestPriority(IDENTIFIER3, USER),
- Optional.of(new PackageAndUser(TARGET, USER)));
+ Optional.of(UserPackage.of(USER, TARGET)));
assertOverlayInfoForTarget(TARGET, USER, o3, o1, o2);
assertEquals(impl.setHighestPriority(IDENTIFIER3, USER),
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
assertOverlayInfoForTarget(TARGET, USER, o1, o2, o3);
assertEquals(impl.setPriority(IDENTIFIER, IDENTIFIER2, USER),
- Optional.of(new PackageAndUser(TARGET, USER)));
+ Optional.of(UserPackage.of(USER, TARGET)));
assertOverlayInfoForTarget(TARGET, USER, o2, o1, o3);
}
@@ -170,47 +169,47 @@
assertNull(impl.getOverlayInfo(IDENTIFIER, USER));
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
assertState(STATE_MISSING_TARGET, IDENTIFIER, USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_DISABLED, IDENTIFIER, USER);
assertEquals(impl.setEnabled(IDENTIFIER, true, USER),
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_ENABLED, IDENTIFIER, USER);
// target upgrades do not change the state of the overlay
upgradeAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)),
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)),
+ Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_ENABLED, IDENTIFIER, USER);
uninstallAndAssert(TARGET, USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_MISSING_TARGET, IDENTIFIER, USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_ENABLED, IDENTIFIER, USER);
}
@Test
public void testOnOverlayPackageUpgraded() throws Exception {
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
upgradeAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)),
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)),
+ Set.of(UserPackage.of(USER, TARGET)));
// upgrade to a version where the overlay has changed its target
upgradeAndAssert(overlay(OVERLAY, TARGET2), USER,
- Set.of(new PackageAndUser(TARGET, USER)),
- Set.of(new PackageAndUser(TARGET, USER),
- new PackageAndUser(TARGET2, USER)));
+ Set.of(UserPackage.of(USER, TARGET)),
+ Set.of(UserPackage.of(USER, TARGET),
+ UserPackage.of(USER, TARGET2)));
}
@Test
@@ -222,10 +221,10 @@
// request succeeded, and there was a change that needs to be
// propagated to the rest of the system
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
- assertEquals(Set.of(new PackageAndUser(TARGET, USER)),
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
+ assertEquals(Set.of(UserPackage.of(USER, TARGET)),
impl.setEnabled(IDENTIFIER, true, USER));
// request succeeded, but nothing changed
@@ -239,9 +238,9 @@
addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_OK), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final FakeIdmapDaemon idmapd = getIdmapd();
final FakeDeviceState state = getState();
@@ -259,9 +258,9 @@
addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final FakeIdmapDaemon idmapd = getIdmapd();
final FakeDeviceState state = getState();
@@ -276,9 +275,9 @@
public void testConfigSignaturePolicyNoConfig() throws Exception {
addPackage(target(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final FakeIdmapDaemon idmapd = getIdmapd();
final FakeDeviceState state = getState();
@@ -292,9 +291,9 @@
@Test
public void testConfigSignaturePolicyNoRefPkg() throws Exception {
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final FakeIdmapDaemon idmapd = getIdmapd();
final FakeDeviceState state = getState();
@@ -312,9 +311,9 @@
addPackage(app(CONFIG_SIGNATURE_REFERENCE_PKG).setCertificate(CERT_CONFIG_OK), USER);
installAndAssert(target(TARGET), USER,
- Set.of(new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, TARGET)));
installAndAssert(overlay(OVERLAY, TARGET).setCertificate(CERT_CONFIG_NOK), USER,
- Set.of(new PackageAndUser(OVERLAY, USER), new PackageAndUser(TARGET, USER)));
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
final FakeIdmapDaemon idmapd = getIdmapd();
final FakeDeviceState state = getState();
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
index 301697d..bba7669 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
@@ -28,6 +28,7 @@
import android.content.om.OverlayInfo;
import android.content.om.OverlayInfo.State;
import android.content.om.OverlayableInfo;
+import android.content.pm.UserPackage;
import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
import android.text.TextUtils;
@@ -164,7 +165,7 @@
* @throws IllegalStateException if the package is currently installed
*/
void installAndAssert(@NonNull FakeDeviceState.PackageBuilder pkg, int userId,
- @NonNull Set<PackageAndUser> onAddedUpdatedPackages)
+ @NonNull Set<UserPackage> onAddedUpdatedPackages)
throws OperationFailedException {
if (mState.select(pkg.packageName, userId) != null) {
throw new IllegalStateException("package " + pkg.packageName + " already installed");
@@ -185,8 +186,8 @@
* @throws IllegalStateException if the package is not currently installed
*/
void upgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId,
- @NonNull Set<PackageAndUser> onReplacingUpdatedPackages,
- @NonNull Set<PackageAndUser> onReplacedUpdatedPackages)
+ @NonNull Set<UserPackage> onReplacingUpdatedPackages,
+ @NonNull Set<UserPackage> onReplacedUpdatedPackages)
throws OperationFailedException {
final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId);
if (replacedPackage == null) {
@@ -207,7 +208,7 @@
* @throws IllegalStateException if the package is not currently installed
*/
void uninstallAndAssert(@NonNull String packageName, int userId,
- @NonNull Set<PackageAndUser> onRemovedUpdatedPackages) {
+ @NonNull Set<UserPackage> onRemovedUpdatedPackages) {
final FakeDeviceState.Package pkg = mState.select(packageName, userId);
if (pkg == null) {
throw new IllegalStateException("package " + packageName + " not installed");
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index fdf9354..0805485 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -74,6 +74,7 @@
import android.content.pm.SigningDetails;
import android.content.pm.SigningInfo;
import android.content.pm.UserInfo;
+import android.content.pm.UserPackage;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Icon;
@@ -97,7 +98,6 @@
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.pm.LauncherAppsService.LauncherAppsImpl;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.uri.UriPermissionOwner;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -692,9 +692,9 @@
protected Map<String, PackageInfo> mInjectedPackages;
- protected Set<PackageWithUser> mUninstalledPackages;
- protected Set<PackageWithUser> mDisabledPackages;
- protected Set<PackageWithUser> mEphemeralPackages;
+ protected Set<UserPackage> mUninstalledPackages;
+ protected Set<UserPackage> mDisabledPackages;
+ protected Set<UserPackage> mEphemeralPackages;
protected Set<String> mSystemPackages;
protected PackageManager mMockPackageManager;
@@ -1200,28 +1200,28 @@
if (ENABLE_DUMP) {
Log.v(TAG, "Uninstall package " + packageName + " / " + userId);
}
- mUninstalledPackages.add(PackageWithUser.of(userId, packageName));
+ mUninstalledPackages.add(UserPackage.of(userId, packageName));
}
protected void installPackage(int userId, String packageName) {
if (ENABLE_DUMP) {
Log.v(TAG, "Install package " + packageName + " / " + userId);
}
- mUninstalledPackages.remove(PackageWithUser.of(userId, packageName));
+ mUninstalledPackages.remove(UserPackage.of(userId, packageName));
}
protected void disablePackage(int userId, String packageName) {
if (ENABLE_DUMP) {
Log.v(TAG, "Disable package " + packageName + " / " + userId);
}
- mDisabledPackages.add(PackageWithUser.of(userId, packageName));
+ mDisabledPackages.add(UserPackage.of(userId, packageName));
}
protected void enablePackage(int userId, String packageName) {
if (ENABLE_DUMP) {
Log.v(TAG, "Enable package " + packageName + " / " + userId);
}
- mDisabledPackages.remove(PackageWithUser.of(userId, packageName));
+ mDisabledPackages.remove(UserPackage.of(userId, packageName));
}
PackageInfo getInjectedPackageInfo(String packageName, @UserIdInt int userId,
@@ -1239,17 +1239,17 @@
ret.applicationInfo.uid = UserHandle.getUid(userId, pi.applicationInfo.uid);
ret.applicationInfo.packageName = pi.packageName;
- if (mUninstalledPackages.contains(PackageWithUser.of(userId, packageName))) {
+ if (mUninstalledPackages.contains(UserPackage.of(userId, packageName))) {
ret.applicationInfo.flags &= ~ApplicationInfo.FLAG_INSTALLED;
}
- if (mEphemeralPackages.contains(PackageWithUser.of(userId, packageName))) {
+ if (mEphemeralPackages.contains(UserPackage.of(userId, packageName))) {
ret.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_INSTANT;
}
if (mSystemPackages.contains(packageName)) {
ret.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
}
ret.applicationInfo.enabled =
- !mDisabledPackages.contains(PackageWithUser.of(userId, packageName));
+ !mDisabledPackages.contains(UserPackage.of(userId, packageName));
if (getSignatures) {
ret.signatures = null;
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 96c0d0a..b20c63c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -83,6 +83,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
+import android.content.pm.UserPackage;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
@@ -107,7 +108,6 @@
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.pm.ShortcutService.ConfigConstants;
import com.android.server.pm.ShortcutService.FileOutputStreamWithPath;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
@@ -4081,12 +4081,12 @@
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_10, LAUNCHER_1),
- PackageWithUser.of(USER_10, LAUNCHER_2)),
+ set(UserPackage.of(USER_10, LAUNCHER_1),
+ UserPackage.of(USER_10, LAUNCHER_2)),
hashSet(user10.getAllLaunchersForTest().keySet()));
assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
"s0_1", "s0_2");
@@ -4113,12 +4113,12 @@
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_10, LAUNCHER_1),
- PackageWithUser.of(USER_10, LAUNCHER_2)),
+ set(UserPackage.of(USER_10, LAUNCHER_1),
+ UserPackage.of(USER_10, LAUNCHER_2)),
hashSet(user10.getAllLaunchersForTest().keySet()));
assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
"s0_1", "s0_2");
@@ -4145,12 +4145,12 @@
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_10, LAUNCHER_1),
- PackageWithUser.of(USER_10, LAUNCHER_2)),
+ set(UserPackage.of(USER_10, LAUNCHER_1),
+ UserPackage.of(USER_10, LAUNCHER_2)),
hashSet(user10.getAllLaunchersForTest().keySet()));
assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
"s0_2");
@@ -4176,11 +4176,11 @@
assertEquals(set(CALLING_PACKAGE_1, CALLING_PACKAGE_2),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+ set(UserPackage.of(USER_10, LAUNCHER_2)),
hashSet(user10.getAllLaunchersForTest().keySet()));
assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
"s0_2");
@@ -4205,11 +4205,11 @@
assertEquals(set(CALLING_PACKAGE_1),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_10, LAUNCHER_2)),
+ set(UserPackage.of(USER_10, LAUNCHER_2)),
hashSet(user10.getAllLaunchersForTest().keySet()));
assertShortcutIds(getLauncherPinnedShortcuts(LAUNCHER_1, USER_0),
"s0_2");
@@ -4234,8 +4234,8 @@
assertEquals(set(CALLING_PACKAGE_1),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(
set(),
@@ -4263,8 +4263,8 @@
assertEquals(set(),
hashSet(user10.getAllPackagesForTest().keySet()));
assertEquals(
- set(PackageWithUser.of(USER_0, LAUNCHER_1),
- PackageWithUser.of(USER_0, LAUNCHER_2)),
+ set(UserPackage.of(USER_0, LAUNCHER_1),
+ UserPackage.of(USER_0, LAUNCHER_2)),
hashSet(user0.getAllLaunchersForTest().keySet()));
assertEquals(set(),
hashSet(user10.getAllLaunchersForTest().keySet()));
@@ -5584,12 +5584,12 @@
assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_3));
assertExistsAndShadow(user0.getAllLaunchersForTest().get(
- PackageWithUser.of(USER_0, LAUNCHER_1)));
+ UserPackage.of(USER_0, LAUNCHER_1)));
assertExistsAndShadow(user0.getAllLaunchersForTest().get(
- PackageWithUser.of(USER_0, LAUNCHER_2)));
+ UserPackage.of(USER_0, LAUNCHER_2)));
- assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_3)));
- assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_P0, LAUNCHER_1)));
+ assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_0, LAUNCHER_3)));
+ assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_P0, LAUNCHER_1)));
doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(any(byte[].class),
anyString());
@@ -6146,12 +6146,12 @@
assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_2));
assertExistsAndShadow(user0.getAllPackagesForTest().get(CALLING_PACKAGE_3));
assertExistsAndShadow(user0.getAllLaunchersForTest().get(
- PackageWithUser.of(USER_0, LAUNCHER_1)));
+ UserPackage.of(USER_0, LAUNCHER_1)));
assertExistsAndShadow(user0.getAllLaunchersForTest().get(
- PackageWithUser.of(USER_0, LAUNCHER_2)));
+ UserPackage.of(USER_0, LAUNCHER_2)));
- assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_0, LAUNCHER_3)));
- assertNull(user0.getAllLaunchersForTest().get(PackageWithUser.of(USER_P0, LAUNCHER_1)));
+ assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_0, LAUNCHER_3)));
+ assertNull(user0.getAllLaunchersForTest().get(UserPackage.of(USER_P0, LAUNCHER_1)));
doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(any(byte[].class),
anyString());
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
index c786784..15fd73c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest2.java
@@ -39,6 +39,7 @@
import android.content.pm.Capability;
import android.content.pm.CapabilityParams;
import android.content.pm.ShortcutInfo;
+import android.content.pm.UserPackage;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
@@ -51,7 +52,6 @@
import androidx.test.filters.SmallTest;
import com.android.frameworks.servicestests.R;
-import com.android.server.pm.ShortcutUser.PackageWithUser;
import java.io.File;
import java.io.FileWriter;
@@ -2413,7 +2413,7 @@
assertWith(mManager.getDynamicShortcuts()).isEmpty();
});
// Make package 1 ephemeral.
- mEphemeralPackages.add(PackageWithUser.of(USER_0, CALLING_PACKAGE_1));
+ mEphemeralPackages.add(UserPackage.of(USER_0, CALLING_PACKAGE_1));
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
assertExpectException(IllegalStateException.class, "Ephemeral apps", () -> {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index d4c9087..35b9710 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1520,6 +1520,29 @@
transition.abort();
}
+ @Test
+ public void testCollectReparentChange() {
+ registerTestTransitionPlayer();
+
+ // Reparent activity in transition.
+ final Task lastParent = createTask(mDisplayContent);
+ final Task newParent = createTask(mDisplayContent);
+ final ActivityRecord activity = createActivityRecord(lastParent);
+ doReturn(true).when(lastParent).shouldRemoveSelfOnLastChildRemoval();
+ doNothing().when(activity).setDropInputMode(anyInt());
+ activity.mVisibleRequested = true;
+
+ final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */,
+ activity.mTransitionController, mWm.mSyncEngine);
+ activity.mTransitionController.moveToCollecting(transition);
+ transition.collect(activity);
+ activity.reparent(newParent, POSITION_TOP);
+
+ // ChangeInfo#mCommonAncestor should be set after reparent.
+ final Transition.ChangeInfo change = transition.mChanges.get(activity);
+ assertEquals(newParent.getDisplayArea(), change.mCommonAncestor);
+ }
+
private static void makeTaskOrganized(Task... tasks) {
final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
for (Task t : tasks) {
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 2c7867c..7be40b8 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8774,6 +8774,22 @@
"premium_capability_purchase_condition_backoff_hysteresis_time_millis_long";
/**
+ * The amount of time in milliseconds within which the network must set up a slicing
+ * configuration for the premium capability after
+ * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}
+ * returns {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_SUCCESS}.
+ * During the setup time, calls to
+ * {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)} will return
+ * {@link TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}.
+ * If the network fails set up a slicing configuration for the premium capability within the
+ * setup time, subsequent purchase requests will be allowed to go through again.
+ *
+ * The default value is 5 minutes.
+ */
+ public static final String KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG =
+ "premium_capability_network_setup_time_millis_long";
+
+ /**
* The URL to redirect to when the user clicks on the notification for a network boost via
* premium capabilities after applications call
* {@link TelephonyManager#purchasePremiumCapability(int, Executor, Consumer)}.
@@ -9496,6 +9512,8 @@
sDefaults.putLong(
KEY_PREMIUM_CAPABILITY_PURCHASE_CONDITION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG,
TimeUnit.MINUTES.toMillis(30));
+ sDefaults.putLong(KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG,
+ TimeUnit.MINUTES.toMillis(5));
sDefaults.putString(KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING, null);
sDefaults.putBoolean(KEY_PREMIUM_CAPABILITY_SUPPORTED_ON_LTE_BOOL, false);
sDefaults.putStringArray(KEY_IWLAN_HANDOVER_POLICY_STRING_ARRAY, new String[]{
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index ef693b5..76a145c 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4002,6 +4002,10 @@
* cautiously, for example, after formatting the number to a consistent format with
* {@link android.telephony.PhoneNumberUtils#formatNumberToE164(String, String)}.
*
+ * <p>The availability and correctness of the phone number depends on the underlying source
+ * and the network etc. Additional verification is needed to use this number for
+ * security-related or other sensitive scenarios.
+ *
* @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID}
* for the default one.
* @return the phone number, or an empty string if not available.
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 5f1d086..35b2055 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17206,7 +17206,13 @@
}
/**
- * Purchase premium capability request was successful. Subsequent attempts will return
+ * Purchase premium capability request was successful.
+ * Once the purchase result is successful, the network must set up a slicing configuration
+ * for the purchased premium capability within the timeout specified by
+ * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG}.
+ * During the setup time, subsequent attempts will return
+ * {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP}.
+ * After setup is complete, subsequent attempts will return
* {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED} until the booster expires.
* The expiry time is determined by the type or duration of boost purchased from the carrier,
* provided at {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_PURCHASE_URL_STRING}.
@@ -17330,6 +17336,16 @@
public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA = 14;
/**
+ * Purchase premium capability was successful and is waiting for the network to setup the
+ * slicing configuration. If the setup is complete within the time specified by
+ * {@link CarrierConfigManager#KEY_PREMIUM_CAPABILITY_NETWORK_SETUP_TIME_MILLIS_LONG},
+ * subsequent requests will return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_ALREADY_PURCHASED}
+ * until the purchase expires. If the setup is not complete within the time specified above,
+ * applications can reques the premium capability again.
+ */
+ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15;
+
+ /**
* Results of the purchase premium capability request.
* @hide
*/
@@ -17347,7 +17363,8 @@
PURCHASE_PREMIUM_CAPABILITY_RESULT_FEATURE_NOT_SUPPORTED,
PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_CONGESTED,
- PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA})
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA,
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
public @interface PurchasePremiumCapabilityResult {}
/**
@@ -17388,6 +17405,8 @@
return "NETWORK_CONGESTED";
case PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA:
return "NOT_DEFAULT_DATA";
+ case PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP:
+ return "PENDING_NETWORK_SETUP";
default:
return "UNKNOWN (" + result + ")";
}