Merge "Zero jank IMF 4/N" into main
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
index 8b55e07..5586295 100644
--- a/apex/jobscheduler/service/Android.bp
+++ b/apex/jobscheduler/service/Android.bp
@@ -28,6 +28,7 @@
static_libs: [
"modules-utils-fastxmlserializer",
+ "service-jobscheduler-alarm.flags-aconfig-java",
"service-jobscheduler-job.flags-aconfig-java",
],
diff --git a/apex/jobscheduler/service/aconfig/Android.bp b/apex/jobscheduler/service/aconfig/Android.bp
index 3ba7aa2..4db39dc 100644
--- a/apex/jobscheduler/service/aconfig/Android.bp
+++ b/apex/jobscheduler/service/aconfig/Android.bp
@@ -27,3 +27,15 @@
aconfig_declarations: "service-job.flags-aconfig",
visibility: ["//frameworks/base:__subpackages__"],
}
+
+// Alarm
+aconfig_declarations {
+ name: "alarm_flags",
+ package: "com.android.server.alarm",
+ srcs: ["alarm.aconfig"],
+}
+
+java_aconfig_library {
+ name: "service-jobscheduler-alarm.flags-aconfig-java",
+ aconfig_declarations: "alarm_flags",
+}
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig
new file mode 100644
index 0000000..3b9b4e7
--- /dev/null
+++ b/apex/jobscheduler/service/aconfig/alarm.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.server.alarm"
+
+flag {
+ name: "use_frozen_state_to_drop_listener_alarms"
+ namespace: "backstage_power"
+ description: "Use frozen state callback to drop listener alarms for cached apps"
+ bug: "324470945"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 39de0af..e728a2c 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.alarm;
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
@@ -75,6 +76,7 @@
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.Activity;
+import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AlarmManager;
@@ -103,6 +105,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
@@ -145,6 +148,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.LocalLog;
@@ -293,6 +297,7 @@
private final Injector mInjector;
int mBroadcastRefCount = 0;
+ boolean mUseFrozenStateToDropListenerAlarms;
MetricsHelper mMetricsHelper;
PowerManager.WakeLock mWakeLock;
SparseIntArray mAlarmsPerUid = new SparseIntArray();
@@ -1856,15 +1861,47 @@
@Override
public void onStart() {
mInjector.init();
+ mHandler = new AlarmHandler();
+
mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false);
mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false);
mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false);
mBroadcastOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false);
+
mMetricsHelper = new MetricsHelper(getContext(), mLock);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
+ if (mUseFrozenStateToDropListenerAlarms) {
+ final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
+ final int size = frozenStates.length;
+ if (uids.length != size) {
+ Slog.wtf(TAG, "Got different length arrays in frozen state callback!"
+ + " uids.length: " + uids.length + " frozenStates.length: " + size);
+ // Cannot process received data in any meaningful way.
+ return;
+ }
+ final IntArray affectedUids = new IntArray();
+ for (int i = 0; i < size; i++) {
+ if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) {
+ continue;
+ }
+ if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED,
+ uids[i])) {
+ continue;
+ }
+ affectedUids.add(uids[i]);
+ }
+ if (affectedUids.size() > 0) {
+ removeExactListenerAlarms(affectedUids.toArray());
+ }
+ };
+ final ActivityManager am = getContext().getSystemService(ActivityManager.class);
+ am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback);
+ }
+
mListenerDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
@@ -1880,7 +1917,6 @@
};
synchronized (mLock) {
- mHandler = new AlarmHandler();
mConstants = new Constants(mHandler);
mAlarmStore = new LazyAlarmStore();
@@ -1960,6 +1996,21 @@
publishBinderService(Context.ALARM_SERVICE, mService);
}
+ private void removeExactListenerAlarms(int... whichUids) {
+ synchronized (mLock) {
+ removeAlarmsInternalLocked(a -> {
+ if (!ArrayUtils.contains(whichUids, a.uid) || a.listener == null
+ || a.windowLength != 0) {
+ return false;
+ }
+ Slog.w(TAG, "Alarm " + a.listenerTag + " being removed for "
+ + UserHandle.formatUid(a.uid) + ":" + a.packageName
+ + " because the app got frozen");
+ return true;
+ }, REMOVE_REASON_LISTENER_CACHED);
+ }
+ }
+
void refreshExactAlarmCandidates() {
final String[] candidates = mLocalPermissionManager.getAppOpPermissionPackages(
Manifest.permission.SCHEDULE_EXACT_ALARM);
@@ -3074,6 +3125,14 @@
mConstants.dump(pw);
pw.println();
+ pw.println("Feature Flags:");
+ pw.increaseIndent();
+ pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
+ mUseFrozenStateToDropListenerAlarms);
+ pw.decreaseIndent();
+ pw.println();
+ pw.println();
+
if (mConstants.USE_TARE_POLICY == EconomyManager.ENABLED_MODE_ON) {
pw.println("TARE details:");
pw.increaseIndent();
@@ -4959,18 +5018,7 @@
break;
case REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED:
uid = (Integer) msg.obj;
- synchronized (mLock) {
- removeAlarmsInternalLocked(a -> {
- if (a.uid != uid || a.listener == null || a.windowLength != 0) {
- return false;
- }
- // TODO (b/265195908): Change to .w once we have some data on breakages.
- Slog.wtf(TAG, "Alarm " + a.listenerTag + " being removed for "
- + UserHandle.formatUid(a.uid) + ":" + a.packageName
- + " because the app went into cached state");
- return true;
- }, REMOVE_REASON_LISTENER_CACHED);
- }
+ removeExactListenerAlarms(uid);
break;
default:
// nope, just ignore it
@@ -5322,6 +5370,10 @@
@Override
public void handleUidCachedChanged(int uid, boolean cached) {
+ if (mUseFrozenStateToDropListenerAlarms) {
+ // Use ActivityManager#UidFrozenStateChangedCallback instead.
+ return;
+ }
if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) {
return;
}
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index d139134..fb1b17b 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -10,6 +10,7 @@
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
+import android.credentials.CredentialOption;
import android.credentials.GetCredentialException;
import android.credentials.GetCredentialRequest;
import android.credentials.GetCredentialResponse;
@@ -29,6 +30,7 @@
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.autofill.FillRequest;
+import android.service.credentials.CredentialProviderService;
import android.text.InputType;
import android.text.Spanned;
import android.text.TextUtils;
@@ -2258,6 +2260,17 @@
@NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
mNode.mGetCredentialRequest = request;
mNode.mGetCredentialCallback = callback;
+ for (CredentialOption option : request.getCredentialOptions()) {
+ ArrayList<AutofillId> ids = option.getCandidateQueryData()
+ .getParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
+ ids = ids != null ? ids : new ArrayList<>();
+ if (!ids.contains(getAutofillId())) {
+ ids.add(getAutofillId());
+ }
+ option.getCandidateQueryData()
+ .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids);
+ }
}
@Override
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 028448c..06dc275 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -85,6 +85,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.credentials.CredentialManager;
+import android.credentials.CredentialOption;
import android.credentials.GetCredentialException;
import android.credentials.GetCredentialRequest;
import android.credentials.GetCredentialResponse;
@@ -129,6 +130,7 @@
import android.os.Trace;
import android.os.Vibrator;
import android.os.vibrator.Flags;
+import android.service.credentials.CredentialProviderService;
import android.sysprop.DisplayProperties;
import android.text.InputType;
import android.text.TextUtils;
@@ -7033,6 +7035,18 @@
@NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
Preconditions.checkNotNull(request, "request must not be null");
Preconditions.checkNotNull(callback, "request must not be null");
+
+ for (CredentialOption option : request.getCredentialOptions()) {
+ ArrayList<AutofillId> ids = option.getCandidateQueryData()
+ .getParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
+ ids = ids != null ? ids : new ArrayList<>();
+ if (!ids.contains(getAutofillId())) {
+ ids.add(getAutofillId());
+ }
+ option.getCandidateQueryData()
+ .putParcelableArrayList(CredentialProviderService.EXTRA_AUTOFILL_ID, ids);
+ }
mViewCredentialHandler = new ViewCredentialHandler(request, callback);
}
diff --git a/core/java/android/view/inputmethod/InputBinding.java b/core/java/android/view/inputmethod/InputBinding.java
index 2bfeb5a..fedee9d 100644
--- a/core/java/android/view/inputmethod/InputBinding.java
+++ b/core/java/android/view/inputmethod/InputBinding.java
@@ -19,11 +19,13 @@
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
/**
* Information given to an {@link InputMethod} about a client connecting
* to it.
*/
+@RavenwoodKeepWholeClass
public final class InputBinding implements Parcelable {
static final String TAG = "InputBinding";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 34be9b0..2606fb6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -114,6 +114,7 @@
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
private boolean mOnBackStartDispatched = false;
+ private boolean mPointerPilfered = false;
private final FlingAnimationUtils mFlingAnimationUtils;
@@ -404,11 +405,12 @@
@VisibleForTesting
void onPilferPointers() {
+ mPointerPilfered = true;
// Dispatch onBackStarted, only to app callbacks.
// System callbacks will receive onBackStarted when the remote animation starts.
if (!shouldDispatchToAnimator() && mActiveCallback != null) {
mCurrentTracker.updateStartLocation();
- tryDispatchAppOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+ tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
}
}
@@ -511,7 +513,7 @@
mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
// App is handling back animation. Cancel system animation latency tracking.
cancelLatencyTracking();
- tryDispatchAppOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+ tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
}
}
@@ -555,14 +557,13 @@
&& mBackNavigationInfo.isPrepareRemoteAnimation();
}
- private void tryDispatchAppOnBackStarted(
+ private void tryDispatchOnBackStarted(
IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
- if (mOnBackStartDispatched && callback != null) {
+ if (mOnBackStartDispatched || callback == null || !mPointerPilfered) {
return;
}
dispatchOnBackStarted(callback, backEvent);
- mOnBackStartDispatched = true;
}
private void dispatchOnBackStarted(
@@ -573,6 +574,7 @@
}
try {
callback.onBackStarted(backEvent);
+ mOnBackStartDispatched = true;
} catch (RemoteException e) {
Log.e(TAG, "dispatchOnBackStarted error: ", e);
}
@@ -872,6 +874,7 @@
mActiveCallback = null;
mShouldStartOnNextMoveEvent = false;
mOnBackStartDispatched = false;
+ mPointerPilfered = false;
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
if (mBackNavigationInfo != null) {
@@ -957,15 +960,7 @@
mCurrentTracker.updateStartLocation();
BackMotionEvent startEvent =
mCurrentTracker.createStartEvent(apps[0]);
- // {@code mActiveCallback} is the callback from
- // the BackAnimationRunners and not a real app-side
- // callback. We also dispatch to the app-side callback
- // (which should be a system callback with PRIORITY_SYSTEM)
- // to keep consistent with app registered callbacks.
dispatchOnBackStarted(mActiveCallback, startEvent);
- tryDispatchAppOnBackStarted(
- mBackNavigationInfo.getOnBackInvokedCallback(),
- startEvent);
}
// Dispatch the first progress after animation start for
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index ef32f6f5..c7daf56 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1996,6 +1996,7 @@
pw.println(innerPrefix + "mToken=" + mToken
+ " binder=" + (mToken != null ? mToken.asBinder() : null));
pw.println(innerPrefix + "mLeash=" + mLeash);
+ pw.println(innerPrefix + "mPipOverlay=" + mPipOverlay);
pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
mPipTransitionController.dump(pw, innerPrefix);
diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
index 5242a7d..c81b95b 100644
--- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -176,11 +176,25 @@
List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled) {
+ this(info, onHost, description, staticAidGroups, dynamicAidGroups,
+ requiresUnlock, requiresScreenOn, bannerResource, uid,
+ settingsActivityName, offHost, staticOffHost, isEnabled,
+ new HashMap<String, Boolean>());
+ }
+
+ /**
+ * @hide
+ */
+ public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
+ List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+ boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
+ String settingsActivityName, String offHost, String staticOffHost, boolean isEnabled,
+ HashMap<String, Boolean> autoTransact) {
this.mService = info;
this.mDescription = description;
this.mStaticAidGroups = new HashMap<String, AidGroup>();
this.mDynamicAidGroups = new HashMap<String, AidGroup>();
- this.mAutoTransact = new HashMap<String, Boolean>();
+ this.mAutoTransact = autoTransact;
this.mOffHostName = offHost;
this.mStaticOffHostName = staticOffHost;
this.mOnHost = onHost;
@@ -196,7 +210,6 @@
this.mUid = uid;
this.mSettingsActivityName = settingsActivityName;
this.mCategoryOtherServiceEnabled = isEnabled;
-
}
/**
@@ -857,6 +870,8 @@
dest.writeString(mSettingsActivityName);
dest.writeInt(mCategoryOtherServiceEnabled ? 1 : 0);
+ dest.writeInt(mAutoTransact.size());
+ dest.writeMap(mAutoTransact);
};
@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
@@ -885,10 +900,15 @@
int uid = source.readInt();
String settingsActivityName = source.readString();
boolean isEnabled = source.readInt() != 0;
+ int autoTransactSize = source.readInt();
+ HashMap<String, Boolean> autoTransact =
+ new HashMap<String, Boolean>(autoTransactSize);
+ source.readMap(autoTransact, getClass().getClassLoader(),
+ String.class, Boolean.class);
return new ApduServiceInfo(info, onHost, description, staticAidGroups,
dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
settingsActivityName, offHostName, staticOffHostName,
- isEnabled);
+ isEnabled, autoTransact);
}
@Override
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 5599ccd..eef75c7 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -40,6 +40,7 @@
import android.service.autofill.FillCallback
import android.service.autofill.FillRequest
import android.service.autofill.FillResponse
+import android.service.autofill.Flags
import android.service.autofill.InlinePresentation
import android.service.autofill.Presentations
import android.service.autofill.SaveCallback
@@ -250,7 +251,7 @@
maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount)
val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver,
Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
- (maxDropdownDisplayLimit - 1)).coerceAtMost(totalEntryCount - 1)
+ (maxDropdownDisplayLimit - 1)).coerceAtMost(totalEntryCount)
var i = 0
var datasetAdded = false
@@ -479,18 +480,28 @@
val autofillIdToCredentialEntries:
MutableMap<AutofillId, ArrayList<Entry>> = mutableMapOf()
credentialEntryList.forEach entryLoop@{ credentialEntry ->
- val autofillId: AutofillId? = credentialEntry
- .frameworkExtrasIntent
- ?.getParcelableExtra(
- CredentialProviderService.EXTRA_AUTOFILL_ID,
- AutofillId::class.java)
- if (autofillId == null) {
- Log.e(TAG, "AutofillId is missing from credential entry. Credential" +
- " Integration might be disabled.")
- return@entryLoop
- }
- autofillIdToCredentialEntries.getOrPut(autofillId) { ArrayList() }
- .add(credentialEntry)
+ val intent = credentialEntry.frameworkExtrasIntent
+ intent?.getParcelableExtra(
+ CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
+ android.service.credentials.GetCredentialRequest::class.java)
+ ?.credentialOptions
+ ?.forEach { credentialOption ->
+ credentialOption.candidateQueryData.getParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+ ?.forEach { autofillId ->
+ intent.putExtra(
+ CredentialProviderService.EXTRA_AUTOFILL_ID,
+ autofillId)
+ val entry = Entry(
+ credentialEntry.key,
+ credentialEntry.subkey,
+ credentialEntry.slice,
+ intent)
+ autofillIdToCredentialEntries
+ .getOrPut(autofillId) { ArrayList() }
+ .add(entry)
+ }
+ }
}
return autofillIdToCredentialEntries
}
@@ -573,23 +584,31 @@
cmRequests: MutableList<CredentialOption>,
responseClientState: Bundle
) {
+ val traversedViewNodes: MutableSet<AutofillId> = mutableSetOf()
+ val credentialOptionsFromHints: MutableMap<String, CredentialOption> = mutableMapOf()
val windowNodes: List<AssistStructure.WindowNode> =
structure.run {
(0 until windowNodeCount).map { getWindowNodeAt(it) }
}
windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
- traverseNodeForRequest(windowNode.rootViewNode, cmRequests, responseClientState)
+ traverseNodeForRequest(
+ windowNode.rootViewNode, cmRequests, responseClientState, traversedViewNodes,
+ credentialOptionsFromHints)
}
}
private fun traverseNodeForRequest(
viewNode: AssistStructure.ViewNode,
cmRequests: MutableList<CredentialOption>,
- responseClientState: Bundle
+ responseClientState: Bundle,
+ traversedViewNodes: MutableSet<AutofillId>,
+ credentialOptionsFromHints: MutableMap<String, CredentialOption>
) {
viewNode.autofillId?.let {
- cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState))
+ cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState,
+ traversedViewNodes, credentialOptionsFromHints))
+ traversedViewNodes.add(it)
}
val children: List<AssistStructure.ViewNode> =
@@ -598,26 +617,37 @@
}
children.forEach { childNode: AssistStructure.ViewNode ->
- traverseNodeForRequest(childNode, cmRequests, responseClientState)
+ traverseNodeForRequest(childNode, cmRequests, responseClientState, traversedViewNodes,
+ credentialOptionsFromHints)
}
}
private fun getCredentialOptionsFromViewNode(
viewNode: AssistStructure.ViewNode,
autofillId: AutofillId,
- responseClientState: Bundle
+ responseClientState: Bundle,
+ traversedViewNodes: MutableSet<AutofillId>,
+ credentialOptionsFromHints: MutableMap<String, CredentialOption>
): MutableList<CredentialOption> {
- if (viewNode.credentialManagerRequest != null) {
- val options = viewNode.credentialManagerRequest?.getCredentialOptions()
- if (options != null) {
- for (option in options) {
- option.candidateQueryData.putParcelable(
- CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId
- )
- }
- return options
+ val credentialOptions: MutableList<CredentialOption> = mutableListOf()
+ if (Flags.autofillCredmanDevIntegration() && viewNode.credentialManagerRequest != null) {
+ viewNode.credentialManagerRequest
+ ?.getCredentialOptions()
+ ?.forEach { credentialOption ->
+ credentialOption.candidateQueryData
+ .getParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+ ?.let { associatedAutofillIds ->
+ // Check whether any of the associated autofill ids have already been
+ // traversed. If so, skip, to dedupe on duplicate credential options.
+ if ((traversedViewNodes intersect associatedAutofillIds.toSet())
+ .isEmpty()) {
+ credentialOptions.add(credentialOption)
+ }
+ }
}
}
+ // TODO(b/325502552): clean up cred option logic in autofill hint
val credentialHints: MutableList<String> = mutableListOf()
if (viewNode.autofillHints != null) {
@@ -631,10 +661,10 @@
}
}
- val credentialOptions: MutableList<CredentialOption> = mutableListOf()
for (credentialHint in credentialHints) {
try {
- convertJsonToCredentialOption(credentialHint, autofillId)
+ convertJsonToCredentialOption(
+ credentialHint, autofillId, credentialOptionsFromHints)
.let { credentialOptions.addAll(it) }
} catch (e: JSONException) {
Log.i(TAG, "Exception while parsing response: " + e.message)
@@ -643,10 +673,11 @@
return credentialOptions
}
- private fun convertJsonToCredentialOption(jsonString: String, autofillId: AutofillId):
- List<CredentialOption> {
- // TODO(b/302000646) Move this logic to jetpack so that is consistent
- // with building the json
+ private fun convertJsonToCredentialOption(
+ jsonString: String,
+ autofillId: AutofillId,
+ credentialOptionsFromHints: MutableMap<String, CredentialOption>
+ ): List<CredentialOption> {
val credentialOptions: MutableList<CredentialOption> = mutableListOf()
val json = JSONObject(jsonString)
@@ -654,16 +685,34 @@
val options = jsonGet.getJSONArray(CRED_OPTIONS_KEY)
for (i in 0 until options.length()) {
val option = options.getJSONObject(i)
- val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY))
- candidateBundle.putParcelable(
+ val optionString = option.toString()
+ credentialOptionsFromHints[optionString]
+ ?.let { credentialOption ->
+ // if the current credential option was seen before, add the current
+ // viewNode to the credential option, but do not add it to the option list
+ // again. This will result in the same result as deduping based on
+ // traversed viewNode.
+ credentialOption.candidateQueryData.getParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId::class.java)
+ ?.let {
+ it.add(autofillId)
+ credentialOption.candidateQueryData.putParcelableArrayList(
+ CredentialProviderService.EXTRA_AUTOFILL_ID, it)
+ }
+ } ?: run {
+ val candidateBundle = convertJsonToBundle(option.getJSONObject(CANDIDATE_DATA_KEY))
+ candidateBundle.putParcelableArrayList(
CredentialProviderService.EXTRA_AUTOFILL_ID,
- autofillId)
- credentialOptions.add(CredentialOption(
+ arrayListOf(autofillId))
+ val credentialOption = CredentialOption(
option.getString(TYPE_KEY),
convertJsonToBundle(option.getJSONObject(REQUEST_DATA_KEY)),
candidateBundle,
option.getBoolean(SYS_PROVIDER_REQ_KEY),
- ))
+ )
+ credentialOptions.add(credentialOption)
+ credentialOptionsFromHints[optionString] = credentialOption
+ }
}
return credentialOptions
}
diff --git a/packages/PrintRecommendationService/res/values/strings.xml b/packages/PrintRecommendationService/res/values/strings.xml
index 2bab1b6..b6c45b7 100644
--- a/packages/PrintRecommendationService/res/values/strings.xml
+++ b/packages/PrintRecommendationService/res/values/strings.xml
@@ -18,7 +18,6 @@
-->
<resources>
- <string name="plugin_vendor_google_cloud_print">Cloud Print</string>
<string name="plugin_vendor_hp">HP</string>
<string name="plugin_vendor_lexmark">Lexmark</string>
<string name="plugin_vendor_brother">Brother</string>
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
index 5a756fe..4ec8883 100644
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
+++ b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
@@ -23,7 +23,6 @@
import android.printservice.recommendation.RecommendationService;
import android.util.Log;
-import com.android.printservice.recommendation.plugin.google.CloudPrintPlugin;
import com.android.printservice.recommendation.plugin.hp.HPRecommendationPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
@@ -77,14 +76,6 @@
}
try {
- mPlugins.add(new RemotePrintServicePlugin(new CloudPrintPlugin(this), this,
- true));
- } catch (Exception e) {
- Log.e(LOG_TAG, "Could not initiate "
- + getString(R.string.plugin_vendor_google_cloud_print) + " plugin", e);
- }
-
- try {
mPlugins.add(new RemotePrintServicePlugin(new HPRecommendationPlugin(this), this,
false));
} catch (Exception e) {
diff --git a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java b/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
deleted file mode 100644
index 3029d10..0000000
--- a/packages/PrintRecommendationService/src/com/android/printservice/recommendation/plugin/google/CloudPrintPlugin.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2017 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.printservice.recommendation.plugin.google;
-
-import static com.android.printservice.recommendation.util.MDNSUtils.ATTRIBUTE_TY;
-
-import android.content.Context;
-import android.util.ArrayMap;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-
-import com.android.printservice.recommendation.PrintServicePlugin;
-import com.android.printservice.recommendation.R;
-import com.android.printservice.recommendation.util.MDNSFilteredDiscovery;
-
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.nio.charset.StandardCharsets;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Plugin detecting <a href="https://developers.google.com/cloud-print/docs/privet">Google Cloud
- * Print</a> printers.
- */
-public class CloudPrintPlugin implements PrintServicePlugin {
- private static final String LOG_TAG = CloudPrintPlugin.class.getSimpleName();
- private static final boolean DEBUG = false;
-
- private static final String ATTRIBUTE_TXTVERS = "txtvers";
- private static final String ATTRIBUTE_URL = "url";
- private static final String ATTRIBUTE_TYPE = "type";
- private static final String ATTRIBUTE_ID = "id";
- private static final String ATTRIBUTE_CS = "cs";
-
- private static final String TYPE = "printer";
-
- private static final String PRIVET_SERVICE = "_privet._tcp";
-
- /** The required mDNS service types */
- private static final Set<String> PRINTER_SERVICE_TYPE = Set.of(
- PRIVET_SERVICE); // Not checking _printer_._sub
-
- /** All possible connection states */
- private static final Set<String> POSSIBLE_CONNECTION_STATES = Set.of(
- "online",
- "offline",
- "connecting",
- "not-configured");
-
- private static final byte SUPPORTED_TXTVERS = '1';
-
- /** The mDNS filtered discovery */
- private final MDNSFilteredDiscovery mMDNSFilteredDiscovery;
-
- /**
- * Create a plugin detecting Google Cloud Print printers.
- *
- * @param context The context the plugin runs in
- */
- public CloudPrintPlugin(@NonNull Context context) {
- mMDNSFilteredDiscovery = new MDNSFilteredDiscovery(context, PRINTER_SERVICE_TYPE,
- nsdServiceInfo -> {
- // The attributes are case insensitive. For faster searching create a clone of
- // the map with the attribute-keys all in lower case.
- ArrayMap<String, byte[]> caseInsensitiveAttributes =
- new ArrayMap<>(nsdServiceInfo.getAttributes().size());
- for (Map.Entry<String, byte[]> entry : nsdServiceInfo.getAttributes()
- .entrySet()) {
- caseInsensitiveAttributes.put(entry.getKey().toLowerCase(),
- entry.getValue());
- }
-
- if (DEBUG) {
- Log.i(LOG_TAG, nsdServiceInfo.getServiceName() + ":");
- Log.i(LOG_TAG, "type: " + nsdServiceInfo.getServiceType());
- Log.i(LOG_TAG, "host: " + nsdServiceInfo.getHost());
- for (Map.Entry<String, byte[]> entry : caseInsensitiveAttributes.entrySet()) {
- if (entry.getValue() == null) {
- Log.i(LOG_TAG, entry.getKey() + "= null");
- } else {
- Log.i(LOG_TAG, entry.getKey() + "=" + new String(entry.getValue(),
- StandardCharsets.UTF_8));
- }
- }
- }
-
- byte[] txtvers = caseInsensitiveAttributes.get(ATTRIBUTE_TXTVERS);
- if (txtvers == null || txtvers.length != 1 || txtvers[0] != SUPPORTED_TXTVERS) {
- // The spec requires this to be the first attribute, but at this time we
- // lost the order of the attributes
- return false;
- }
-
- if (caseInsensitiveAttributes.get(ATTRIBUTE_TY) == null) {
- return false;
- }
-
- byte[] url = caseInsensitiveAttributes.get(ATTRIBUTE_URL);
- if (url == null || url.length == 0) {
- return false;
- }
-
- byte[] type = caseInsensitiveAttributes.get(ATTRIBUTE_TYPE);
- if (type == null || !TYPE.equals(
- new String(type, StandardCharsets.UTF_8).toLowerCase())) {
- return false;
- }
-
- if (caseInsensitiveAttributes.get(ATTRIBUTE_ID) == null) {
- return false;
- }
-
- byte[] cs = caseInsensitiveAttributes.get(ATTRIBUTE_CS);
- if (cs == null || !POSSIBLE_CONNECTION_STATES.contains(
- new String(cs, StandardCharsets.UTF_8).toLowerCase())) {
- return false;
- }
-
- InetAddress address = nsdServiceInfo.getHost();
- if (!(address instanceof Inet4Address)) {
- // Not checking for link local address
- return false;
- }
-
- return true;
- });
- }
-
- @Override
- @NonNull public CharSequence getPackageName() {
- return "com.google.android.apps.cloudprint";
- }
-
- @Override
- public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
- mMDNSFilteredDiscovery.start(callback);
- }
-
- @Override
- @StringRes public int getName() {
- return R.string.plugin_vendor_google_cloud_print;
- }
-
- @Override
- public void stop() throws Exception {
- mMDNSFilteredDiscovery.stop();
- }
-}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index e185367..761bb79 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -27,7 +27,7 @@
import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider
import com.android.settingslib.spa.gallery.dialog.NavDialogProvider
import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
-import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider
+import com.android.settingslib.spa.gallery.editor.SettingsDropdownBoxPageProvider
import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider
import com.android.settingslib.spa.gallery.home.HomePageProvider
import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
@@ -99,7 +99,7 @@
OperateListPageProvider,
EditorMainPageProvider,
SettingsOutlinedTextFieldPageProvider,
- SettingsExposedDropdownMenuBoxPageProvider,
+ SettingsDropdownBoxPageProvider,
SettingsDropdownCheckBoxProvider,
SettingsTextFieldPasswordPageProvider,
SearchScaffoldPageProvider,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
index 9f2158a..c511542 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
@@ -35,7 +35,7 @@
return listOf(
SettingsOutlinedTextFieldPageProvider.buildInjectEntry().setLink(fromPage = owner)
.build(),
- SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
+ SettingsDropdownBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
.build(),
SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner)
.build(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
similarity index 63%
rename from packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
rename to packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
index 5ffbe8ba..2ebb5f5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuBoxPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownBoxPageProvider.kt
@@ -28,16 +28,15 @@
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuBox
+import com.android.settingslib.spa.widget.editor.SettingsDropdownBox
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
-private const val TITLE = "Sample SettingsExposedDropdownMenuBox"
+private const val TITLE = "Sample SettingsDropdownBox"
-object SettingsExposedDropdownMenuBoxPageProvider : SettingsPageProvider {
- override val name = "SettingsExposedDropdownMenuBox"
- private const val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
+object SettingsDropdownBoxPageProvider : SettingsPageProvider {
+ override val name = "SettingsDropdownBox"
override fun getTitle(arguments: Bundle?): String {
return TITLE
@@ -45,18 +44,44 @@
@Composable
override fun Page(arguments: Bundle?) {
- var selectedItem by remember { mutableIntStateOf(-1) }
- val options = listOf("item1", "item2", "item3")
RegularScaffold(title = TITLE) {
- SettingsExposedDropdownMenuBox(
- label = exposedDropdownMenuBoxLabel,
- options = options,
- selectedOptionIndex = selectedItem,
- enabled = true,
- onselectedOptionTextChange = { selectedItem = it })
+ Regular()
+ NotEnabled()
+ Empty()
}
}
+ @Composable
+ private fun Regular() {
+ var selectedItem by remember { mutableIntStateOf(-1) }
+ SettingsDropdownBox(
+ label = "SettingsDropdownBox",
+ options = listOf("item1", "item2", "item3"),
+ selectedOptionIndex = selectedItem,
+ ) { selectedItem = it }
+ }
+
+ @Composable
+ private fun NotEnabled() {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ SettingsDropdownBox(
+ label = "Not enabled",
+ options = listOf("item1", "item2", "item3"),
+ enabled = false,
+ selectedOptionIndex = selectedItem,
+ ) { selectedItem = it }
+ }
+
+ @Composable
+ private fun Empty() {
+ var selectedItem by remember { mutableIntStateOf(-1) }
+ SettingsDropdownBox(
+ label = "Empty",
+ options = emptyList(),
+ selectedOptionIndex = selectedItem,
+ ) { selectedItem = it }
+ }
+
fun buildInjectEntry(): SettingsEntryBuilder {
return SettingsEntryBuilder.createInject(owner = createSettingsPage())
.setUiLayoutFn {
@@ -70,8 +95,8 @@
@Preview(showBackground = true)
@Composable
-private fun SettingsExposedDropdownMenuBoxPagePreview() {
+private fun SettingsDropdownBoxPagePreview() {
SettingsTheme {
- SettingsExposedDropdownMenuBoxPageProvider.Page(null)
+ SettingsDropdownBoxPageProvider.Page(null)
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt
new file mode 100644
index 0000000..679c562
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/DropdownTextBox.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+
+internal interface DropdownTextBoxScope {
+ fun dismiss()
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DropdownTextBox(
+ label: String,
+ text: String,
+ enabled: Boolean = true,
+ errorMessage: String? = null,
+ content: @Composable DropdownTextBoxScope.() -> Unit,
+) {
+ var expanded by remember { mutableStateOf(false) }
+ val scope = remember {
+ object : DropdownTextBoxScope {
+ override fun dismiss() {
+ expanded = false
+ }
+ }
+ }
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = enabled && it },
+ modifier = Modifier
+ .padding(SettingsDimension.menuFieldPadding)
+ .width(Width),
+ ) {
+ OutlinedTextField(
+ // The `menuAnchor` modifier must be passed to the text field for correctness.
+ modifier = Modifier
+ .menuAnchor()
+ .fillMaxWidth(),
+ value = text,
+ onValueChange = { },
+ label = { Text(text = label) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ singleLine = true,
+ readOnly = true,
+ enabled = enabled,
+ isError = errorMessage != null,
+ supportingText = errorMessage?.let { { Text(text = it) } },
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ modifier = Modifier.width(Width),
+ onDismissRequest = { expanded = false },
+ ) { scope.content() }
+ }
+}
+
+private val Width = 310.dp
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt
new file mode 100644
index 0000000..ff141c2
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBox.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.editor
+
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun SettingsDropdownBox(
+ label: String,
+ options: List<String>,
+ selectedOptionIndex: Int,
+ enabled: Boolean = true,
+ onSelectedOptionChange: (Int) -> Unit,
+) {
+ DropdownTextBox(
+ label = label,
+ text = options.getOrElse(selectedOptionIndex) { "" },
+ enabled = enabled && options.isNotEmpty(),
+ ) {
+ options.forEachIndexed { index, option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = {
+ dismiss()
+ onSelectedOptionChange(index)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun SettingsDropdownBoxPreview() {
+ val item1 = "item1"
+ val item2 = "item2"
+ val item3 = "item3"
+ val options = listOf(item1, item2, item3)
+ SettingsTheme {
+ SettingsDropdownBox(
+ label = "ExposedDropdownMenuBoxLabel",
+ options = options,
+ selectedOptionIndex = 0,
+ enabled = true,
+ ) {}
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
index 57963e6..0e7e499 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
@@ -19,28 +19,15 @@
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -68,7 +55,6 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsDropdownCheckBox(
label: String,
@@ -78,43 +64,18 @@
errorMessage: String? = null,
onSelectedStateChange: () -> Unit = {},
) {
- var dropDownWidth by remember { mutableIntStateOf(0) }
- var expanded by remember { mutableStateOf(false) }
- val changeable = enabled && options.changeable
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = { expanded = changeable && it },
- modifier = Modifier
- .width(350.dp)
- .padding(SettingsDimension.textFieldPadding)
- .onSizeChanged { dropDownWidth = it.width },
+ DropdownTextBox(
+ label = label,
+ text = getDisplayText(options) ?: emptyText,
+ enabled = enabled && options.changeable,
+ errorMessage = errorMessage,
) {
- OutlinedTextField(
- // The `menuAnchor` modifier must be passed to the text field for correctness.
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth(),
- value = getDisplayText(options) ?: emptyText,
- onValueChange = {},
- label = { Text(text = label) },
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
- readOnly = true,
- enabled = changeable,
- isError = errorMessage != null,
- supportingText = errorMessage?.let { { Text(text = it) } },
- )
- ExposedDropdownMenu(
- expanded = expanded,
- modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
- onDismissRequest = { expanded = false },
- ) {
- for (option in options) {
- CheckboxItem(option) {
- option.onClick()
- if (option.changeable) {
- checkboxItemOnClick(options, option)
- onSelectedStateChange()
- }
+ for (option in options) {
+ CheckboxItem(option) {
+ option.onClick()
+ if (option.changeable) {
+ checkboxItemOnClick(options, option)
+ onSelectedStateChange()
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
deleted file mode 100644
index f6692a3..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBox.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2023 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.settingslib.spa.widget.editor
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-
-@Composable
-@OptIn(ExperimentalMaterial3Api::class)
-fun SettingsExposedDropdownMenuBox(
- label: String,
- options: List<String>,
- selectedOptionIndex: Int,
- enabled: Boolean,
- onselectedOptionTextChange: (Int) -> Unit,
-) {
- var expanded by remember { mutableStateOf(false) }
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = { expanded = it },
- modifier = Modifier
- .width(350.dp)
- .padding(SettingsDimension.menuFieldPadding),
- ) {
- OutlinedTextField(
- // The `menuAnchor` modifier must be passed to the text field for correctness.
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth(),
- value = options.getOrElse(selectedOptionIndex) { "" },
- onValueChange = { },
- label = { Text(text = label) },
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(
- expanded = expanded
- )
- },
- singleLine = true,
- readOnly = true,
- enabled = enabled
- )
- if (options.isNotEmpty()) {
- ExposedDropdownMenu(
- expanded = expanded,
- modifier = Modifier
- .fillMaxWidth(),
- onDismissRequest = { expanded = false },
- ) {
- options.forEach { option ->
- DropdownMenuItem(
- text = { Text(option) },
- onClick = {
- onselectedOptionTextChange(options.indexOf(option))
- expanded = false
- },
- contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
- )
- }
- }
- }
- }
-}
-
-@Preview
-@Composable
-private fun SettingsExposedDropdownMenuBoxsPreview() {
- val item1 = "item1"
- val item2 = "item2"
- val item3 = "item3"
- val options = listOf(item1, item2, item3)
- SettingsTheme {
- SettingsExposedDropdownMenuBox(
- label = "ExposedDropdownMenuBoxLabel",
- options = options,
- selectedOptionIndex = 0,
- enabled = true,
- onselectedOptionTextChange = {})
- }
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt
new file mode 100644
index 0000000..c347424
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownBoxTest.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsDropdownBoxTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun dropdownMenuBox_displayed() {
+ composeTestRule.setContent {
+ var selectedItem by remember { mutableStateOf(0) }
+ SettingsDropdownBox(
+ label = LABEL,
+ options = options,
+ selectedOptionIndex = selectedItem,
+ ) { selectedItem = it }
+ }
+
+ composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+ }
+
+ @Test
+ fun dropdownMenuBox_enabled_expanded() {
+ composeTestRule.setContent {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ SettingsDropdownBox(
+ label = LABEL,
+ options = options,
+ selectedOptionIndex = selectedItem
+ ) { selectedItem = it }
+ }
+ composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+ composeTestRule.onNodeWithText(LABEL).performClick()
+
+ composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed()
+ }
+
+ @Test
+ fun dropdownMenuBox_notEnabled_notExpanded() {
+ composeTestRule.setContent {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ SettingsDropdownBox(
+ label = LABEL,
+ options = options,
+ enabled = false,
+ selectedOptionIndex = selectedItem
+ ) { selectedItem = it }
+ }
+ composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+ composeTestRule.onNodeWithText(LABEL).performClick()
+
+ composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+ }
+
+ @Test
+ fun dropdownMenuBox_valueChanged() {
+ composeTestRule.setContent {
+ var selectedItem by remember { mutableIntStateOf(0) }
+ SettingsDropdownBox(
+ label = LABEL,
+ options = options,
+ selectedOptionIndex = selectedItem
+ ) { selectedItem = it }
+ }
+ composeTestRule.onNodeWithText(ITEM2).assertDoesNotExist()
+
+ composeTestRule.onNodeWithText(LABEL).performClick()
+ composeTestRule.onNodeWithText(ITEM2).performClick()
+
+ composeTestRule.onNodeWithText(ITEM2).assertIsDisplayed()
+ }
+ private companion object {
+ const val LABEL = "Label"
+ const val ITEM2 = "item2"
+ val options = listOf("item1", ITEM2, "item3")
+ }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
deleted file mode 100644
index bc67e4c..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuBoxTest.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2023 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.settingslib.spa.widget.editor
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SettingsExposedDropdownMenuBoxTest {
- @get:Rule
- val composeTestRule = createComposeRule()
- private val options = listOf("item1", "item2", "item3")
- private val item2 = "item2"
- private val exposedDropdownMenuBoxLabel = "ExposedDropdownMenuBoxLabel"
-
- @Test
- fun exposedDropdownMenuBoxs_displayed() {
- composeTestRule.setContent {
- var selectedItem by remember { mutableStateOf(0) }
- SettingsExposedDropdownMenuBox(
- label = exposedDropdownMenuBoxLabel,
- options = options,
- selectedOptionIndex = selectedItem,
- enabled = true,
- onselectedOptionTextChange = { selectedItem = it })
- }
- composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
- .assertIsDisplayed()
- }
-
- @Test
- fun exposedDropdownMenuBoxs_expanded() {
- composeTestRule.setContent {
- var selectedItem by remember { mutableIntStateOf(0) }
- SettingsExposedDropdownMenuBox(
- label = exposedDropdownMenuBoxLabel,
- options = options,
- selectedOptionIndex = selectedItem,
- enabled = true,
- onselectedOptionTextChange = { selectedItem = it })
- }
- composeTestRule.onNodeWithText(item2, substring = true)
- .assertDoesNotExist()
- composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
- .performClick()
- composeTestRule.onNodeWithText(item2, substring = true)
- .assertIsDisplayed()
- }
-
- @Test
- fun exposedDropdownMenuBoxs_valueChanged() {
- composeTestRule.setContent {
- var selectedItem by remember { mutableIntStateOf(0) }
- SettingsExposedDropdownMenuBox(
- label = exposedDropdownMenuBoxLabel,
- options = options,
- selectedOptionIndex = selectedItem,
- enabled = true,
- onselectedOptionTextChange = { selectedItem = it })
- }
- composeTestRule.onNodeWithText(item2, substring = true)
- .assertDoesNotExist()
- composeTestRule.onNodeWithText(exposedDropdownMenuBoxLabel, substring = true)
- .performClick()
- composeTestRule.onNodeWithText(item2, substring = true)
- .performClick()
- composeTestRule.onNodeWithText(item2, substring = true)
- .assertIsDisplayed()
- }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index cddd4fa..622a4f0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -73,6 +73,7 @@
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -85,7 +86,10 @@
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -110,6 +114,7 @@
import com.android.systemui.res.R
import kotlinx.coroutines.launch
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CommunalHub(
modifier: Modifier = Modifier,
@@ -140,6 +145,8 @@
Box(
modifier =
modifier
+ .semantics { testTagsAsResourceId = true }
+ .testTag(COMMUNAL_HUB_TEST_TAG)
.fillMaxSize()
.pointerInput(gridState, contentOffset, contentListState) {
// If not in edit mode, don't allow selecting items.
@@ -872,3 +879,6 @@
)
val IconSize = 48.dp
}
+
+/** The resource id of communal hub accessible from UiAutomator. */
+private const val COMMUNAL_HUB_TEST_TAG = "communal_hub"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index ed2cbb8..c7d43fc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -55,7 +55,7 @@
notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
ambientState: AmbientState,
notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main private val mainDispatcher: CoroutineDispatcher,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
) {
init {
@@ -80,7 +80,7 @@
sceneContainerFlags,
controller,
notificationStackSizeCalculator,
- mainDispatcher,
+ mainImmediateDispatcher = mainImmediateDispatcher,
)
if (sceneContainerFlags.flexiNotifsEnabled()) {
@@ -90,6 +90,7 @@
notificationStackAppearanceViewModel,
ambientState,
controller,
+ mainImmediateDispatcher = mainImmediateDispatcher,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index d0f57c7..02c889d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -111,7 +111,7 @@
sceneContainerFlags,
controller,
notificationStackSizeCalculator,
- mainDispatcher,
+ mainImmediateDispatcher = mainDispatcher,
)
)
@@ -123,6 +123,7 @@
notificationStackAppearanceViewModel,
ambientState,
controller,
+ mainImmediateDispatcher = mainDispatcher,
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
index a157785..76495cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
@@ -20,12 +20,14 @@
import android.util.TypedValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
@@ -40,8 +42,9 @@
viewModel: NotificationStackAppearanceViewModel,
ambientState: AmbientState,
controller: NotificationStackScrollLayoutController,
+ @Main mainImmediateDispatcher: CoroutineDispatcher,
): DisposableHandle {
- return view.repeatWhenAttached {
+ return view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
viewModel.stackBounds.collect { bounds ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 373d209..d2e0386 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -801,8 +801,9 @@
Log.d(TAG, "teardown: entered");
setOrientation(mOriginalOrientation);
Log.d(TAG, "teardown: after setOrientation");
- mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
- Log.d(TAG, "teardown: after advanceTimeBy");
+ // Unclear why we used to do this, and it seems to be a source of flakes
+ // mAnimatorTestRule.advanceTimeBy(mLongestHideShowAnimationDuration);
+ Log.d(TAG, "teardown: skipped advanceTimeBy");
mTestableLooper.moveTimeForward(mLongestHideShowAnimationDuration);
Log.d(TAG, "teardown: after moveTimeForward");
mTestableLooper.processAllMessages();
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index d8a94d8..e0fe88a 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -1277,6 +1277,20 @@
}
@Override
+ public void onCaptureFailed(int captureSequenceId, int reason) {
+ if (Flags.concertMode()) {
+ if (mCaptureCallback != null) {
+ try {
+ mCaptureCallback.onCaptureProcessFailed(captureSequenceId, reason);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to notify capture failure due to remote " +
+ "exception!");
+ }
+ }
+ }
+ }
+
+ @Override
public void onCaptureSequenceCompleted(int captureSequenceId) {
if (mCaptureCallback != null) {
try {
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 4a4c290..eb3c55c 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -255,6 +255,7 @@
android.view.Display$HdrCapabilities
android.view.Display$Mode
android.view.DisplayInfo
+android.view.inputmethod.InputBinding
android.hardware.SerialManager
android.hardware.SerialManagerInternal
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index d4f049e..a754ba5 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -73,6 +73,16 @@
}
flag {
+ name: "handle_multi_device_input"
+ namespace: "accessibility"
+ description: "Select a single active device when a multi-device stream is received by AccessibilityInputFilter"
+ bug: "310014874"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "pinch_zoom_zero_min_span"
namespace: "accessibility"
description: "Whether to set min span of ScaleGestureDetector to zero."
@@ -87,6 +97,16 @@
}
flag {
+ name: "reset_hover_event_timer_on_action_up"
+ namespace: "accessibility"
+ description: "Reset the timer for sending hover events on receiving ACTION_UP to guarantee the correct amount of time is available between taps."
+ bug: "326260351"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "scan_packages_without_lock"
namespace: "accessibility"
description: "Scans packages for accessibility service/activity info without holding the A11yMS lock"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index abcd8e2..16119d11 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.graphics.Region;
import android.os.PowerManager;
+import android.os.SystemClock;
import android.provider.Settings;
import android.util.Slog;
import android.util.SparseArray;
@@ -35,6 +36,8 @@
import android.view.InputFilter;
import android.view.KeyEvent;
import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
import android.view.accessibility.AccessibilityEvent;
import com.android.server.LocalServices;
@@ -203,6 +206,62 @@
private EventStreamState mKeyboardStreamState;
+ /**
+ * The last MotionEvent emitted from the input device that's currently active. This is used to
+ * keep track of which input device is currently active, and also to generate the cancel event
+ * if a new device becomes active.
+ */
+ private MotionEvent mLastActiveDeviceMotionEvent = null;
+
+ private static MotionEvent cancelMotion(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
+ || event.getActionMasked() == MotionEvent.ACTION_UP) {
+ throw new IllegalArgumentException("Can't cancel " + event);
+ }
+ final int action;
+ if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
+ || event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
+ action = MotionEvent.ACTION_HOVER_EXIT;
+ } else {
+ action = MotionEvent.ACTION_CANCEL;
+ }
+
+ final int pointerCount;
+ if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+ pointerCount = event.getPointerCount() - 1;
+ } else {
+ pointerCount = event.getPointerCount();
+ }
+ final PointerProperties[] properties = new PointerProperties[pointerCount];
+ final PointerCoords[] coords = new PointerCoords[pointerCount];
+ int newPointerIndex = 0;
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+ if (event.getActionIndex() == i) {
+ // Skip the pointer that's going away
+ continue;
+ }
+ }
+ final PointerCoords c = new PointerCoords();
+ c.x = event.getX(i);
+ c.y = event.getY(i);
+ coords[newPointerIndex] = c;
+ final PointerProperties p = new PointerProperties();
+ p.id = event.getPointerId(i);
+ p.toolType = event.getToolType(i);
+ properties[newPointerIndex] = p;
+ newPointerIndex++;
+ }
+
+ return MotionEvent.obtain(event.getDownTime(), SystemClock.uptimeMillis(), action,
+ pointerCount, properties, coords,
+ event.getMetaState(), event.getButtonState(),
+ event.getXPrecision(), event.getYPrecision(), event.getDeviceId(),
+ event.getEdgeFlags(), event.getSource(), event.getDisplayId(), event.getFlags(),
+ event.getClassification());
+ }
+
AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
this(context, service, new SparseArray<>(0));
}
@@ -260,6 +319,17 @@
AccessibilityTrace.FLAGS_INPUT_FILTER,
"event=" + event + ";policyFlags=" + policyFlags);
}
+ if (Flags.handleMultiDeviceInput()) {
+ if (!shouldProcessMultiDeviceEvent(event, policyFlags)) {
+ // We are only allowing a single device to be active at a time.
+ return;
+ }
+ }
+
+ onInputEventInternal(event, policyFlags);
+ }
+
+ private void onInputEventInternal(InputEvent event, int policyFlags) {
if (mEventHandler.size() == 0) {
if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
super.onInputEvent(event, policyFlags);
@@ -353,6 +423,63 @@
}
}
+ boolean shouldProcessMultiDeviceEvent(InputEvent event, int policyFlags) {
+ if (event instanceof MotionEvent motion) {
+ // Only allow 1 device to be sending motion events at a time
+ // If the event is from an active device, let it through.
+ // If the event is not from an active device, only let it through if it starts a new
+ // gesture like ACTION_DOWN or ACTION_HOVER_ENTER
+ final boolean eventIsFromCurrentDevice = mLastActiveDeviceMotionEvent != null
+ && mLastActiveDeviceMotionEvent.getDeviceId() == motion.getDeviceId();
+ final int actionMasked = motion.getActionMasked();
+ switch (actionMasked) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_HOVER_ENTER:
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ if (mLastActiveDeviceMotionEvent != null
+ && mLastActiveDeviceMotionEvent.getDeviceId() != motion.getDeviceId()) {
+ // This is a new gesture from a new device. Cancel the existing state
+ // and let this through
+ MotionEvent canceled = cancelMotion(mLastActiveDeviceMotionEvent);
+ onInputEventInternal(canceled, policyFlags);
+ }
+ mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE:
+ case MotionEvent.ACTION_POINTER_DOWN:
+ case MotionEvent.ACTION_POINTER_UP: {
+ if (eventIsFromCurrentDevice) {
+ mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ if (eventIsFromCurrentDevice) {
+ // This is the last event of the gesture from this device.
+ mLastActiveDeviceMotionEvent = null;
+ return true;
+ } else {
+ // Event is from another device
+ return false;
+ }
+ }
+ default: {
+ if (mLastActiveDeviceMotionEvent != null
+ && event.getDeviceId() != mLastActiveDeviceMotionEvent.getDeviceId()) {
+ // This is an event from another device, ignore it.
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
super.onInputEvent(event, policyFlags);
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
index 9b27dd3..40b6ff0 100644
--- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -232,9 +232,63 @@
}
/** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
- private static boolean isBrailleDisplay(byte[] descriptor) {
- // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
- return true;
+ @VisibleForTesting
+ static boolean isBrailleDisplay(byte[] descriptor) {
+ boolean foundMatch = false;
+ for (int i = 0; i < descriptor.length; i++) {
+ // HID Spec "6.2.2.2 Short Items" defines that the report descriptor is a collection of
+ // items: each item is a collection of bytes where the first byte defines info about
+ // the type of item and the following 0, 1, 2, or 4 bytes are data bytes for that item.
+ // All items in the HID descriptor are expected to be Short Items.
+ final byte itemInfo = descriptor[i];
+ if (!isHidItemShort(itemInfo)) {
+ Slog.w(LOG_TAG, "Item " + itemInfo + " declares unsupported long type");
+ return false;
+ }
+ final int dataSize = getHidItemDataSize(itemInfo);
+ if (i + dataSize >= descriptor.length) {
+ Slog.w(LOG_TAG, "Item " + itemInfo + " specifies size past the remaining bytes");
+ return false;
+ }
+ // The item we're looking for (usage page declaration) should have size 1.
+ if (dataSize == 1) {
+ final byte itemData = descriptor[i + 1];
+ if (isHidItemBrailleDisplayUsagePage(itemInfo, itemData)) {
+ foundMatch = true;
+ }
+ }
+ // Move to the next item by skipping past all data bytes in this item.
+ i += dataSize;
+ }
+ return foundMatch;
+ }
+
+ private static boolean isHidItemShort(byte itemInfo) {
+ // Info bits 7-4 describe the item type, and HID Spec "6.2.2.3 Long Items" says that long
+ // items always have type bits 1111. Otherwise, the item is a short item.
+ return (itemInfo & 0b1111_0000) != 0b1111_0000;
+ }
+
+ private static int getHidItemDataSize(byte itemInfo) {
+ // HID Spec "6.2.2.2 Short Items" says that info bits 0-1 specify the optional data size:
+ // 0, 1, 2, or 4 bytes.
+ return switch (itemInfo & 0b0000_0011) {
+ case 0b00 -> 0;
+ case 0b01 -> 1;
+ case 0b10 -> 2;
+ default -> 4;
+ };
+ }
+
+ private static boolean isHidItemBrailleDisplayUsagePage(byte itemInfo, byte itemData) {
+ // From HID Spec "6.2.2.7 Global Items"
+ final byte usagePageType = 0b0000_0100;
+ // From HID Usage Tables version 1.2.
+ final byte brailleDisplayUsagePage = 0x41;
+ // HID Spec "6.2.2.2 Short Items" says item info bits 2-7 describe the type and
+ // function of the item.
+ final byte itemType = (byte) (itemInfo & 0b1111_1100);
+ return itemType == usagePageType && itemData == brailleDisplayUsagePage;
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
index b6223c7..bf9202f1b 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
@@ -106,11 +106,30 @@
return;
}
}
+ final long downTime;
if (action == MotionEvent.ACTION_DOWN) {
- event.setDownTime(event.getEventTime());
+ downTime = event.getEventTime();
} else {
- event.setDownTime(mState.getLastInjectedDownEventTime());
+ downTime = mState.getLastInjectedDownEventTime();
}
+
+ // The only way to change device id of the motion event is by re-creating the whole thing
+ final PointerProperties[] properties = new PointerProperties[event.getPointerCount()];
+ final PointerCoords[] coords = new PointerCoords[event.getPointerCount()];
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ final PointerCoords c = new PointerCoords();
+ event.getPointerCoords(i, c);
+ coords[i] = c;
+ final PointerProperties p = new PointerProperties();
+ event.getPointerProperties(i, p);
+ properties[i] = p;
+ }
+ event = MotionEvent.obtain(downTime, event.getEventTime(), event.getAction(),
+ event.getPointerCount(), properties, coords,
+ event.getMetaState(), event.getButtonState(),
+ event.getXPrecision(), event.getYPrecision(), rawEvent.getDeviceId(),
+ event.getEdgeFlags(), rawEvent.getSource(), event.getDisplayId(), event.getFlags(),
+ event.getClassification());
// If the user is long pressing but the long pressing pointer
// was not exactly over the accessibility focused item we need
// to remap the location of that pointer so the user does not
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 3086ce1..4fc65bf 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -852,6 +852,11 @@
final int pointerIdBits = (1 << pointerId);
if (mSendHoverEnterAndMoveDelayed.isPending()) {
// If we have not delivered the enter schedule an exit.
+ if (Flags.resetHoverEventTimerOnActionUp()) {
+ // We cancel first to reset the time window so that the user has the full amount of
+ // time to do a multi tap.
+ mSendHoverEnterAndMoveDelayed.repost();
+ }
mSendHoverExitDelayed.post(event, rawEvent, pointerIdBits, policyFlags);
} else {
// The user is touch exploring so we send events for end.
@@ -1554,6 +1559,14 @@
}
}
+ public void repost() {
+ // cancel without clearing
+ if (isPending()) {
+ mHandler.removeCallbacks(this);
+ mHandler.postDelayed(this, mDetermineUserIntentTimeout);
+ }
+ }
+
private boolean isPending() {
return mHandler.hasCallbacks(this);
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
index 84a59b4..7251ac4 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodClientInvoker.java
@@ -43,6 +43,9 @@
* the given {@link Handler} thread if {@link IInputMethodClient} is not a proxy object. Be careful
* about its call ordering characteristics.</p>
*/
+// TODO(b/322895594) Mark this class to be host side test compatible once enabling fw/services in
+// Ravenwood (mark this class with @RavenwoodKeepWholeClass and #create with @RavenwoodReplace,
+// so Ravenwood can properly swap create method during test execution).
final class IInputMethodClientInvoker {
private static final String TAG = InputMethodManagerService.TAG;
private static final boolean DEBUG = InputMethodManagerService.DEBUG;
@@ -64,6 +67,16 @@
return new IInputMethodClientInvoker(inputMethodClient, isProxy, isProxy ? null : handler);
}
+ @AnyThread
+ @Nullable
+ static IInputMethodClientInvoker create$ravenwood(
+ @Nullable IInputMethodClient inputMethodClient, @NonNull Handler handler) {
+ if (inputMethodClient == null) {
+ return null;
+ }
+ return new IInputMethodClientInvoker(inputMethodClient, true, null);
+ }
+
private IInputMethodClientInvoker(@NonNull IInputMethodClient target,
boolean isProxy, @Nullable Handler handler) {
mTarget = target;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 9b347d5..bf06c2a 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -15430,17 +15430,18 @@
mPerDisplayBatteryStats[i].screenStateAtLastEnergyMeasurement = screenState;
}
- final boolean compatibleConfig;
if (supportedStandardBuckets != null) {
final EnergyConsumerStats.Config config = new EnergyConsumerStats.Config(
supportedStandardBuckets, customBucketNames,
SUPPORTED_PER_PROCESS_STATE_STANDARD_ENERGY_BUCKETS,
getBatteryConsumerProcessStateNames());
- if (mEnergyConsumerStatsConfig == null) {
- compatibleConfig = true;
- } else {
- compatibleConfig = mEnergyConsumerStatsConfig.isCompatible(config);
+ if (mEnergyConsumerStatsConfig != null
+ && !mEnergyConsumerStatsConfig.isCompatible(config)) {
+ // Supported power buckets changed since last boot.
+ // Existing data is no longer reliable.
+ resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
+ RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
}
mEnergyConsumerStatsConfig = config;
@@ -15456,18 +15457,14 @@
mWifiPowerCalculator = new WifiPowerCalculator(mPowerProfile);
}
} else {
- compatibleConfig = (mEnergyConsumerStatsConfig == null);
- // EnergyConsumer no longer supported, wipe out the existing data.
+ if (mEnergyConsumerStatsConfig != null) {
+ // EnergyConsumer no longer supported, wipe out the existing data.
+ resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
+ RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
+ }
mEnergyConsumerStatsConfig = null;
mGlobalEnergyConsumerStats = null;
}
-
- if (!compatibleConfig) {
- // Supported power buckets changed since last boot.
- // Existing data is no longer reliable.
- resetAllStatsLocked(SystemClock.uptimeMillis(), SystemClock.elapsedRealtime(),
- RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE);
- }
}
@GuardedBy("this")
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 10cbc66..85d81c4 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -104,6 +104,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.ToBooleanFunction;
import com.android.server.am.HostingRecord;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.window.flags.Flags;
@@ -3025,11 +3026,17 @@
return false;
}
- // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
- return forAllWindows(
+ ToBooleanFunction<WindowState> getDimBehindWindow =
(w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
&& w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
- || w.mActivityRecord.isVisible()), true);
+ || w.mActivityRecord.isVisible());
+ if (adjacentTf.forAllWindows(getDimBehindWindow, true)) {
+ // early return if the adjacent Tf has a dimming window.
+ return false;
+ }
+
+ // boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
+ return forAllWindows(getDimBehindWindow, true);
}
@Override
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index ca23d62..fa63bc8 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -30,9 +30,7 @@
import android.credentials.selection.Entry;
import android.credentials.selection.GetCredentialProviderData;
import android.credentials.selection.ProviderPendingIntentResponse;
-import android.os.Bundle;
import android.os.ICancellationSignal;
-import android.service.autofill.Flags;
import android.service.credentials.Action;
import android.service.credentials.BeginGetCredentialOption;
import android.service.credentials.BeginGetCredentialRequest;
@@ -44,7 +42,6 @@
import android.service.credentials.RemoteEntry;
import android.util.Pair;
import android.util.Slog;
-import android.view.autofill.AutofillId;
import java.util.ArrayList;
import java.util.HashMap;
@@ -77,10 +74,6 @@
@NonNull
private final Map<String, CredentialOption> mBeginGetOptionToCredentialOptionMap;
- @NonNull
- private final Map<String, AutofillId> mCredentialEntryKeyToAutofilLIdMap;
-
-
/** The complete request to be used in the second round. */
private final android.credentials.GetCredentialRequest mCompleteRequest;
private final CallingAppInfo mCallingAppInfo;
@@ -249,7 +242,6 @@
mBeginGetOptionToCredentialOptionMap = new HashMap<>(beginGetOptionToCredentialOptionMap);
mProviderResponseDataHandler = new ProviderResponseDataHandler(
ComponentName.unflattenFromString(hybridService));
- mCredentialEntryKeyToAutofilLIdMap = new HashMap<>();
}
/** Called when the provider response has been updated by an external source. */
@@ -303,7 +295,7 @@
invokeCallbackOnInternalInvalidState();
return;
}
- onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+ onCredentialEntrySelected(providerPendingIntentResponse);
break;
case ACTION_ENTRY_KEY:
Action actionEntry = mProviderResponseDataHandler.getActionEntry(entryKey);
@@ -312,7 +304,7 @@
invokeCallbackOnInternalInvalidState();
return;
}
- onActionEntrySelected(providerPendingIntentResponse, entryKey);
+ onActionEntrySelected(providerPendingIntentResponse);
break;
case AUTHENTICATION_ACTION_ENTRY_KEY:
Action authenticationEntry = mProviderResponseDataHandler
@@ -342,7 +334,7 @@
break;
case REMOTE_ENTRY_KEY:
if (mProviderResponseDataHandler.getRemoteEntry(entryKey) != null) {
- onRemoteEntrySelected(providerPendingIntentResponse, entryKey);
+ onRemoteEntrySelected(providerPendingIntentResponse);
} else {
Slog.i(TAG, "Unexpected remote entry key");
invokeCallbackOnInternalInvalidState();
@@ -381,7 +373,7 @@
return null;
}
- private Intent setUpFillInIntentWithFinalRequest(@NonNull String id, String entryKey) {
+ private Intent setUpFillInIntentWithFinalRequest(@NonNull String id) {
// TODO: Determine if we should skip this entry if entry id is not set, or is set
// but does not resolve to a valid option. For now, not skipping it because
// it may be possible that the provider adds their own extras and expects to receive
@@ -392,13 +384,6 @@
Slog.w(TAG, "Id from Credential Entry does not resolve to a valid option");
return intent;
}
- AutofillId autofillId = credentialOption
- .getCandidateQueryData()
- .getParcelable(CredentialProviderService.EXTRA_AUTOFILL_ID, AutofillId.class);
- if (autofillId != null && Flags.autofillCredmanIntegration()) {
- intent.putExtra(CredentialProviderService.EXTRA_AUTOFILL_ID, autofillId);
- mCredentialEntryKeyToAutofilLIdMap.put(entryKey, autofillId);
- }
return intent.putExtra(
CredentialProviderService.EXTRA_GET_CREDENTIAL_REQUEST,
new GetCredentialRequest(
@@ -414,13 +399,12 @@
}
private void onRemoteEntrySelected(
- ProviderPendingIntentResponse providerPendingIntentResponse, String entryKey) {
- onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+ ProviderPendingIntentResponse providerPendingIntentResponse) {
+ onCredentialEntrySelected(providerPendingIntentResponse);
}
private void onCredentialEntrySelected(
- ProviderPendingIntentResponse providerPendingIntentResponse,
- String entryKey) {
+ ProviderPendingIntentResponse providerPendingIntentResponse) {
if (providerPendingIntentResponse == null) {
invokeCallbackOnInternalInvalidState();
return;
@@ -437,18 +421,7 @@
GetCredentialResponse getCredentialResponse = PendingIntentResultHandler
.extractGetCredentialResponse(
providerPendingIntentResponse.getResultData());
- if (getCredentialResponse != null && getCredentialResponse.getCredential() != null) {
- Bundle credentialData = getCredentialResponse.getCredential().getData();
- AutofillId autofillId = mCredentialEntryKeyToAutofilLIdMap.get(entryKey);
- if (Flags.autofillCredmanIntegration()
- && entryKey != null && autofillId != null && credentialData != null
- ) {
- Slog.d(TAG, "Adding autofillId to credential response: " + autofillId);
- credentialData.putParcelable(
- CredentialProviderService.EXTRA_AUTOFILL_ID,
- mCredentialEntryKeyToAutofilLIdMap.get(entryKey)
- );
- }
+ if (getCredentialResponse != null) {
mCallbacks.onFinalResponseReceived(mComponentName,
getCredentialResponse);
return;
@@ -532,9 +505,9 @@
/** Returns true if either an exception or a response is found. */
private void onActionEntrySelected(ProviderPendingIntentResponse
- providerPendingIntentResponse, String entryKey) {
+ providerPendingIntentResponse) {
Slog.i(TAG, "onActionEntrySelected");
- onCredentialEntrySelected(providerPendingIntentResponse, entryKey);
+ onCredentialEntrySelected(providerPendingIntentResponse);
}
@@ -632,7 +605,7 @@
Entry entry = new Entry(CREDENTIAL_ENTRY_KEY,
id, credentialEntry.getSlice(),
setUpFillInIntentWithFinalRequest(credentialEntry
- .getBeginGetCredentialOptionId(), id));
+ .getBeginGetCredentialOptionId()));
mUiCredentialEntries.put(id, new Pair<>(credentialEntry, entry));
mCredentialEntryTypes.add(credentialEntry.getType());
}
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index afd6dbd..b7af58c 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -85,7 +85,6 @@
srcs: [
"src/com/android/server/inputmethod/**/ClientControllerTest.java",
],
- sdk_version: "test_current",
auto_gen_config: true,
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index dc9631a..9e3d9ec 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -32,10 +32,8 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import android.view.Display;
-import android.view.inputmethod.InputBinding;
import com.android.internal.inputmethod.IInputMethodClient;
import com.android.internal.inputmethod.IRemoteInputConnection;
@@ -53,7 +51,7 @@
public final class ClientControllerTest {
private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY;
private static final int ANY_CALLER_UID = 1;
- private static final int ANY_CALLER_PID = 1;
+ private static final int ANY_CALLER_PID = 2;
private static final String SOME_PACKAGE_NAME = "some.package";
@Rule
@@ -82,13 +80,16 @@
mController = new ClientController(mMockPackageManagerInternal);
}
- @Test
- // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
- // inputmethod server classes.
- @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
- public void testAddClient_cannotAddTheSameClientTwice() {
- var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+ // TODO(b/322895594): No need to directly invoke create$ravenwood once b/322895594 is fixed.
+ private IInputMethodClientInvoker createInvoker(IInputMethodClient client, Handler handler) {
+ return RavenwoodRule.isOnRavenwood()
+ ? IInputMethodClientInvoker.create$ravenwood(client, handler) :
+ IInputMethodClientInvoker.create(client, handler);
+ }
+ @Test
+ public void testAddClient_cannotAddTheSameClientTwice() {
+ final var invoker = createInvoker(mClient, mHandler);
synchronized (ImfLock.class) {
mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
ANY_CALLER_PID);
@@ -101,18 +102,17 @@
}
});
assertThat(thrown.getMessage()).isEqualTo(
- "uid=1/pid=1/displayId=0 is already registered");
+ "uid=" + ANY_CALLER_UID + "/pid=" + ANY_CALLER_PID
+ + "/displayId=0 is already registered");
}
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
- // inputmethod server classes.
- @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
public void testAddClient() throws Exception {
+ final var invoker = createInvoker(mClient, mHandler);
synchronized (ImfLock.class) {
- var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
- var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+ final var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID,
+ ANY_CALLER_UID,
ANY_CALLER_PID);
verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0));
@@ -121,16 +121,12 @@
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
- // inputmethod server classes.
- @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
public void testRemoveClient() {
- var callback = new TestClientControllerCallback();
+ final var invoker = createInvoker(mClient, mHandler);
+ final var callback = new TestClientControllerCallback();
ClientState added;
synchronized (ImfLock.class) {
mController.addClientControllerCallback(callback);
-
- var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
ANY_CALLER_PID);
assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added);
@@ -138,21 +134,17 @@
}
// Test callback
- var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
+ final var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS);
assertThat(removed).isSameInstanceAs(added);
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
- // inputmethod server classes and updated to newer Mockito with static mock support (mock
- // InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp)
- @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class})
public void testVerifyClientAndPackageMatch() {
+ final var invoker = createInvoker(mClient, mHandler);
when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */
anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true);
synchronized (ImfLock.class) {
- var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
ANY_CALLER_PID);
assertThat(
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index a476155..9975221 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.alarm;
import static android.Manifest.permission.SCHEDULE_EXACT_ALARM;
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
+import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static android.app.AlarmManager.EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED;
@@ -42,6 +44,7 @@
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.NULL_DEFAULT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
@@ -135,6 +138,7 @@
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
@@ -145,9 +149,11 @@
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.flag.util.FlagSetException;
import android.provider.DeviceConfig;
-import android.provider.Settings;
import android.text.format.DateFormat;
import android.util.ArraySet;
import android.util.Log;
@@ -215,6 +221,7 @@
private AppStateTrackerImpl.Listener mListener;
private AlarmManagerService.UninstallReceiver mPackageChangesReceiver;
private AlarmManagerService.ChargingReceiver mChargingReceiver;
+ private ActivityManager.UidFrozenStateChangedCallback mUidFrozenStateCallback;
private IAppOpsCallback mIAppOpsCallback;
private IAlarmManager mBinder;
@Mock
@@ -240,6 +247,8 @@
@Mock
private ActivityManagerInternal mActivityManagerInternal;
@Mock
+ private ActivityManager mActivityManager;
+ @Mock
private PackageManagerInternal mPackageManagerInternal;
@Mock
private AppStateTrackerImpl mAppStateTracker;
@@ -403,15 +412,31 @@
.mockStatic(PermissionChecker.class)
.mockStatic(PermissionManagerService.class)
.mockStatic(ServiceManager.class)
- .mockStatic(Settings.Global.class)
.mockStatic(SystemProperties.class)
.spyStatic(UserHandle.class)
.afterSessionFinished(
() -> LocalServices.removeServiceForTest(AlarmManagerInternal.class))
.build();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(NULL_DEFAULT);
+
+ /**
+ * Have to do this to switch the {@link Flags} implementation to {@link FakeFeatureFlagsImpl}.
+ * All methods that need any flag enabled should use the
+ * {@link android.platform.test.annotations.EnableFlags} annotation, in which case disabling
+ * the flag will fail with an exception that we will swallow here.
+ */
+ private void disableFlagsNotSetByAnnotation() {
+ try {
+ mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS);
+ } catch (FlagSetException fse) {
+ // Expected if the test about to be run requires this enabled.
+ }
+ }
+
@Before
- public final void setUp() {
+ public void setUp() {
doReturn(mIActivityManager).when(ActivityManager::getService);
doReturn(mDeviceIdleInternal).when(
() -> LocalServices.getService(DeviceIdleInternal.class));
@@ -469,6 +494,7 @@
when(mMockContext.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(mAppOpsManager);
when(mMockContext.getSystemService(BatteryManager.class)).thenReturn(mBatteryManager);
+ when(mMockContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
registerAppIds(new String[]{TEST_CALLING_PACKAGE},
new Integer[]{UserHandle.getAppId(TEST_CALLING_UID)});
@@ -479,7 +505,18 @@
mService = new AlarmManagerService(mMockContext, mInjector);
spyOn(mService);
+ disableFlagsNotSetByAnnotation();
+
mService.onStart();
+
+ if (Flags.useFrozenStateToDropListenerAlarms()) {
+ final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor =
+ ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class);
+ verify(mActivityManager).registerUidFrozenStateChangedCallback(
+ any(HandlerExecutor.class), frozenCaptor.capture());
+ mUidFrozenStateCallback = frozenCaptor.getValue();
+ }
+
// Unable to mock mMockContext to return a mock stats manager.
// So just mocking the whole MetricsHelper instance.
mService.mMetricsHelper = mock(MetricsHelper.class);
@@ -3741,9 +3778,87 @@
mListener.handleUidCachedChanged(TEST_CALLING_UID, true);
assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+ assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true);
assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED);
+ assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+ assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+ }
+
+ private void executeUidFrozenStateCallback(int[] uids, int[] frozenStates) {
+ assertNotNull(mUidFrozenStateCallback);
+ mUidFrozenStateCallback.onUidFrozenStateChanged(uids, frozenStates);
+ }
+
+ @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+ @Test
+ public void exactListenerAlarmsRemovedOnFrozen() {
+ mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
+
+ setTestAlarmWithListener(ELAPSED_REALTIME, 31, getNewListener(() -> {}), WINDOW_EXACT,
+ TEST_CALLING_UID);
+ setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
+ setTestAlarm(ELAPSED_REALTIME, 54, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+ TEST_CALLING_UID, null);
+ setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
+
+ setTestAlarmWithListener(ELAPSED_REALTIME, 21, getNewListener(() -> {}), WINDOW_EXACT,
+ TEST_CALLING_UID_2);
+ setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
+ setTestAlarm(ELAPSED_REALTIME, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+ TEST_CALLING_UID_2, null);
+ setTestAlarm(RTC, 549, 234, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID_2, null);
+
+ assertEquals(8, mService.mAlarmStore.size());
+
+ executeUidFrozenStateCallback(
+ new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2},
+ new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN});
+ assertEquals(7, mService.mAlarmStore.size());
+
+ executeUidFrozenStateCallback(
+ new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN});
+ assertEquals(6, mService.mAlarmStore.size());
+ }
+
+ @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+ @Test
+ public void alarmCountOnListenerFrozen() {
+ mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
+
+ // Set some alarms for TEST_CALLING_UID.
+ final int numExactListenerUid1 = 17;
+ for (int i = 0; i < numExactListenerUid1; i++) {
+ setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
+ getNewListener(() -> {}));
+ }
+ setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID);
+ setTestAlarm(ELAPSED_REALTIME, 54, getNewMockPendingIntent());
+ setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null);
+
+ // Set some alarms for TEST_CALLING_UID_2.
+ final int numExactListenerUid2 = 11;
+ for (int i = 0; i < numExactListenerUid2; i++) {
+ setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i,
+ getNewListener(() -> {}), WINDOW_EXACT, TEST_CALLING_UID_2);
+ }
+ setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2);
+ setTestAlarm(RTC_WAKEUP, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0,
+ TEST_CALLING_UID_2, null);
+
+ assertEquals(numExactListenerUid1 + 3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+ assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+
+ executeUidFrozenStateCallback(
+ new int[] {TEST_CALLING_UID, TEST_CALLING_UID_2},
+ new int[] {UID_FROZEN_STATE_FROZEN, UID_FROZEN_STATE_UNFROZEN});
+ assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
+ assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
+
+ executeUidFrozenStateCallback(
+ new int[] {TEST_CALLING_UID_2}, new int[] {UID_FROZEN_STATE_FROZEN});
+ assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID));
assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2));
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 6cd79bc..ae6984e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -18,12 +18,17 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.content.Context;
import android.hardware.SensorManager;
+import android.os.AggregateBatteryConsumer;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryStats;
@@ -34,6 +39,7 @@
import android.os.Process;
import android.os.UidBatteryConsumer;
import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.SparseLongArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -517,6 +523,57 @@
}
@Test
+ public void saveBatteryUsageStatsOnReset_incompatibleEnergyConsumers() throws Throwable {
+ MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+ batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
+ int componentId0 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
+ int componentId1 = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID + 1;
+
+ synchronized (batteryStats) {
+ batteryStats.getUidStatsLocked(APP_UID);
+
+ SparseLongArray uidEnergies = new SparseLongArray();
+ uidEnergies.put(APP_UID, 30_000_000);
+ batteryStats.updateCustomEnergyConsumerStatsLocked(0, 100_000_000, uidEnergies);
+ batteryStats.updateCustomEnergyConsumerStatsLocked(1, 200_000_000, uidEnergies);
+ }
+
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
+ mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
+
+ PowerStatsStore powerStatsStore = mock(PowerStatsStore.class);
+ doAnswer(invocation -> {
+ BatteryUsageStats stats = invocation.getArgument(1);
+ AggregateBatteryConsumer device = stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE);
+ assertThat(device.getCustomPowerComponentName(componentId0)).isEqualTo("FOO");
+ assertThat(device.getCustomPowerComponentName(componentId1)).isEqualTo("BAR");
+ assertThat(device.getConsumedPowerForCustomComponent(componentId0))
+ .isWithin(PRECISION).of(27.77777);
+ assertThat(device.getConsumedPowerForCustomComponent(componentId1))
+ .isWithin(PRECISION).of(55.55555);
+
+ UidBatteryConsumer uid = stats.getUidBatteryConsumers().get(0);
+ assertThat(uid.getConsumedPowerForCustomComponent(componentId0))
+ .isWithin(PRECISION).of(8.33333);
+ assertThat(uid.getConsumedPowerForCustomComponent(componentId1))
+ .isWithin(PRECISION).of(8.33333);
+ return null;
+ }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+
+ mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+
+ // Make an incompatible change of supported energy components. This will trigger
+ // a BatteryStats reset, which will generate a snapshot of battery stats.
+ mStatsRule.initMeasuredEnergyStatsLocked(
+ new String[]{"COMPONENT1"});
+
+ mStatsRule.waitForBackgroundThread();
+
+ verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+ }
+
+ @Test
public void testAggregateBatteryStats_incompatibleSnapshot() {
MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index 8bdb029..7e8fa55 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -16,6 +16,8 @@
package com.android.server.power.stats;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -28,6 +30,7 @@
import android.os.BatteryStats;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
+import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.UidBatteryConsumer;
@@ -74,6 +77,7 @@
private NetworkStats mNetworkStats;
private boolean[] mSupportedStandardBuckets;
private String[] mCustomPowerComponentNames;
+ private Throwable mThrowable;
public BatteryUsageStatsRule() {
this(0, null);
@@ -270,6 +274,7 @@
public void evaluate() throws Throwable {
before();
base.evaluate();
+ after();
}
};
}
@@ -277,6 +282,9 @@
private void before() {
lateInitBatteryStats();
HandlerThread bgThread = new HandlerThread("bg thread");
+ bgThread.setUncaughtExceptionHandler((thread, throwable)-> {
+ mThrowable = throwable;
+ });
bgThread.start();
mHandler = new Handler(bgThread.getLooper());
mBatteryStats.setHandler(mHandler);
@@ -285,6 +293,26 @@
mBatteryStats.getOnBatteryScreenOffTimeBase().setRunning(!mScreenOn, 0, 0);
}
+ private void after() throws Throwable {
+ if (mHandler != null) {
+ waitForBackgroundThread();
+ }
+ }
+
+ public void waitForBackgroundThread() throws Throwable {
+ if (mThrowable != null) {
+ throw mThrowable;
+ }
+
+ ConditionVariable done = new ConditionVariable();
+ mHandler.post(done::open);
+ assertThat(done.block(10000)).isTrue();
+
+ if (mThrowable != null) {
+ throw mThrowable;
+ }
+ }
+
public PowerProfile getPowerProfile() {
return mPowerProfile;
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
index 52c7d8d..5c8c6bb 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt
@@ -17,31 +17,38 @@
import android.hardware.display.DisplayManagerGlobal
import android.os.SystemClock
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import android.view.DisplayAdjustments
import android.view.DisplayInfo
import android.view.IInputFilterHost
+import android.view.InputDevice.SOURCE_STYLUS
import android.view.InputDevice.SOURCE_TOUCHSCREEN
import android.view.InputEvent
import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_CANCEL
import android.view.MotionEvent.ACTION_DOWN
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.MotionEvent.ACTION_MOVE
+import android.view.MotionEvent.ACTION_UP
import android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.cts.input.inputeventmatchers.withDeviceId
import com.android.cts.input.inputeventmatchers.withMotionAction
+import com.android.cts.input.inputeventmatchers.withSource
import com.android.server.LocalServices
import com.android.server.accessibility.magnification.MagnificationProcessor
import com.android.server.wm.WindowManagerInternal
import java.util.concurrent.LinkedBlockingQueue
import org.hamcrest.Matchers.allOf
import org.junit.After
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -92,12 +99,17 @@
or AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER
or AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS
or AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS)
+ const val STYLUS_SOURCE = SOURCE_STYLUS or SOURCE_TOUCHSCREEN
}
@Rule
@JvmField
val mocks: MockitoRule = MockitoJUnit.rule()
+ @Rule
+ @JvmField
+ val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Mock
private lateinit var mockA11yController: WindowManagerInternal.AccessibilityControllerInternal
@@ -115,6 +127,9 @@
private lateinit var ams: AccessibilityManagerService
private lateinit var a11yInputFilter: AccessibilityInputFilter
private val touchDeviceId = 1
+ private val fromTouchScreen = allOf(withDeviceId(touchDeviceId), withSource(SOURCE_TOUCHSCREEN))
+ private val stylusDeviceId = 2
+ private val fromStylus = allOf(withDeviceId(stylusDeviceId), withSource(STYLUS_SOURCE))
@Before
fun setUp() {
@@ -156,23 +171,14 @@
enableFeatures(0)
val downTime = SystemClock.uptimeMillis()
- val downEvent = createMotionEvent(
- ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
- send(downEvent)
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_DOWN), withDeviceId(touchDeviceId)))
+ sendTouchEvent(ACTION_DOWN, downTime, downTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_DOWN)))
- val moveEvent = createMotionEvent(
- ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(moveEvent)
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
+ sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_MOVE)))
- val upEvent = createMotionEvent(
- ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(upEvent)
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+ sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_UP)))
verifier.assertNoEvents()
}
@@ -186,28 +192,91 @@
enableFeatures(ALL_A11Y_FEATURES)
val downTime = SystemClock.uptimeMillis()
- val downEvent = createMotionEvent(
- ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
- send(MotionEvent.obtain(downEvent))
-
+ sendTouchEvent(ACTION_DOWN, downTime, downTime)
// DOWN event gets transformed to HOVER_ENTER
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
// MOVE becomes HOVER_MOVE
- val moveEvent = createMotionEvent(
- ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(moveEvent)
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_HOVER_MOVE), withDeviceId(touchDeviceId)))
+ sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_MOVE)))
// UP becomes HOVER_EXIT
- val upEvent = createMotionEvent(
- ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(upEvent)
+ sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Enable all a11y features and send a touchscreen stream of DOWN -> CANCEL -> DOWN events.
+ * These get converted into HOVER_ENTER -> HOVER_EXIT -> HOVER_ENTER events by the input filter.
+ */
+ @Test
+ fun testTouchDownCancelDownWithAllA11yFeatures() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ val downTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, downTime, downTime)
+ // DOWN event gets transformed to HOVER_ENTER
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // CANCEL becomes HOVER_EXIT
+ sendTouchEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+
+ // DOWN again! New hover is expected
+ val newDownTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, newDownTime, newDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Enable all a11y features and send a stylus stream of DOWN -> CANCEL -> DOWN events.
+ * These get converted into HOVER_ENTER -> HOVER_EXIT -> HOVER_ENTER events by the input filter.
+ * This test is the same as above, but for stylus events.
+ */
+ @Test
+ fun testStylusDownCancelDownWithAllA11yFeatures() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ val downTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, downTime, downTime)
+ // DOWN event gets transformed to HOVER_ENTER
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // CANCEL becomes HOVER_EXIT
+ sendStylusEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+ // DOWN again! New hover is expected
+ val newDownTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, newDownTime, newDownTime)
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Enable all a11y features and send a stylus stream and then a touch stream.
+ */
+ @Test
+ fun testStylusThenTouch() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ val downTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, downTime, downTime)
+ // DOWN event gets transformed to HOVER_ENTER
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // CANCEL becomes HOVER_EXIT
+ sendStylusEvent(ACTION_CANCEL, downTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+ val newDownTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, newDownTime, newDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
verifier.assertNoEvents()
}
@@ -223,26 +292,18 @@
enableFeatures(ALL_A11Y_FEATURES)
val downTime = SystemClock.uptimeMillis()
- val downEvent = createMotionEvent(
- ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId)
- send(MotionEvent.obtain(downEvent))
+ sendTouchEvent(ACTION_DOWN, downTime, downTime)
// DOWN event gets transformed to HOVER_ENTER
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
verifier.assertNoEvents()
enableFeatures(0)
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
verifier.assertNoEvents()
- val moveEvent = createMotionEvent(
- ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(moveEvent)
- val upEvent = createMotionEvent(
- ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId)
- send(upEvent)
+ sendTouchEvent(ACTION_MOVE, downTime, SystemClock.uptimeMillis())
+ sendTouchEvent(ACTION_UP, downTime, SystemClock.uptimeMillis())
// As the original gesture continues, no additional events should be getting sent by the
// filter because the HOVER_EXIT above already effectively finished the current gesture and
// the DOWN event was never sent to the host.
@@ -250,10 +311,148 @@
// Bug: the down event was swallowed, so the remainder of the gesture should be swallowed
// too. However, the MOVE and UP events are currently passed back to the dispatcher.
// TODO(b/310014874) - ensure a11y sends consistent input streams to the dispatcher
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId)))
- verifier.assertReceivedMotion(
- allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_MOVE)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_UP)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Check multi-device behaviour when all a11y features are disabled. The events should pass
+ * through unmodified, but only from the active (first) device.
+ * The events from the inactive device should be dropped.
+ * In this test, we are injecting a touchscreen event stream and a stylus event stream,
+ * interleaved.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+ fun testMultiDeviceEventsWithoutA11yFeatures() {
+ enableFeatures(0)
+
+ val touchDownTime = SystemClock.uptimeMillis()
+
+ // Touch device - ACTION_DOWN
+ sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_DOWN)))
+
+ // Stylus device - ACTION_DOWN
+ val stylusDownTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_CANCEL)))
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_DOWN)))
+
+ // Touch device - ACTION_MOVE
+ sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+ // Touch event is dropped
+ verifier.assertNoEvents()
+
+ // Stylus device - ACTION_MOVE
+ sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_MOVE)))
+
+ // Touch device - ACTION_UP
+ sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+ // Touch event is dropped
+ verifier.assertNoEvents()
+
+ // Stylus device - ACTION_UP
+ sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_UP)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Check multi-device behaviour when all a11y features are enabled. The events should be
+ * modified accordingly, like DOWN events getting converted to hovers.
+ * Only a single device should be active (the latest device to start a new gesture).
+ * In this test, we are injecting a touchscreen event stream and a stylus event stream,
+ * interleaved.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+ fun testMultiDeviceEventsWithAllA11yFeatures() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ // Touch device - ACTION_DOWN
+ val touchDownTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // Stylus device - ACTION_DOWN
+ val stylusDownTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+ // Touch is canceled and stylus is started
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // Touch device - ACTION_MOVE
+ sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+ // Stylus is active now; touch is ignored
+ verifier.assertNoEvents()
+
+ // Stylus device - ACTION_MOVE
+ sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_MOVE)))
+
+ // Touch device - ACTION_UP
+ sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+ // Stylus is still active; touch is ignored
+ verifier.assertNoEvents()
+
+ sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+
+ // Now stylus is done, and a new touch gesture will work!
+ val newTouchDownTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, newTouchDownTime, newTouchDownTime)
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+ verifier.assertNoEvents()
+ }
+
+ /**
+ * Check multi-device behaviour when all a11y features are enabled. The events should be
+ * modified accordingly, like DOWN events getting converted to hovers.
+ * Only a single device should be active at a given time. The touch events start and end
+ * while stylus is active. Check that the latest device is always given preference.
+ */
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_HANDLE_MULTI_DEVICE_INPUT)
+ fun testStylusWithTouchInTheMiddle() {
+ enableFeatures(ALL_A11Y_FEATURES)
+
+ // Stylus device - ACTION_DOWN
+ val stylusDownTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, stylusDownTime, stylusDownTime)
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // Touch device - ACTION_DOWN
+ val touchDownTime = SystemClock.uptimeMillis()
+ sendTouchEvent(ACTION_DOWN, touchDownTime, touchDownTime)
+ // Touch DOWN causes stylus to get canceled
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_EXIT)))
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_ENTER)))
+
+ // Touch device - ACTION_MOVE
+ sendTouchEvent(ACTION_MOVE, touchDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_MOVE)))
+
+ sendStylusEvent(ACTION_MOVE, stylusDownTime, SystemClock.uptimeMillis())
+ // Stylus is ignored because touch is active now
+ verifier.assertNoEvents()
+
+ sendTouchEvent(ACTION_UP, touchDownTime, SystemClock.uptimeMillis())
+ verifier.assertReceivedMotion(allOf(fromTouchScreen, withMotionAction(ACTION_HOVER_EXIT)))
+
+ sendStylusEvent(ACTION_UP, stylusDownTime, SystemClock.uptimeMillis())
+ // The UP stylus event is also ignored
+ verifier.assertNoEvents()
+
+ // Now stylus works again, because touch gesture is finished
+ val newStylusDownTime = SystemClock.uptimeMillis()
+ sendStylusEvent(ACTION_DOWN, newStylusDownTime, newStylusDownTime)
+ verifier.assertReceivedMotion(allOf(fromStylus, withMotionAction(ACTION_HOVER_ENTER)))
verifier.assertNoEvents()
}
@@ -264,6 +463,20 @@
return display
}
+ private fun sendTouchEvent(action: Int, downTime: Long, eventTime: Long) {
+ if (action == ACTION_DOWN) {
+ assertEquals(downTime, eventTime)
+ }
+ send(createMotionEvent(action, downTime, eventTime, SOURCE_TOUCHSCREEN, touchDeviceId))
+ }
+
+ private fun sendStylusEvent(action: Int, downTime: Long, eventTime: Long) {
+ if (action == ACTION_DOWN) {
+ assertEquals(downTime, eventTime)
+ }
+ send(createMotionEvent(action, downTime, eventTime, STYLUS_SOURCE, stylusDeviceId))
+ }
+
private fun send(event: InputEvent) {
// We need to make a copy of the event before sending it to the filter, because the filter
// will recycle it, but the caller of this function might want to still be able to use
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
index b322dd7..aec3f45 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -17,6 +17,7 @@
package com.android.server.accessibility;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -33,17 +34,24 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.internal.util.HexDump;
+
import com.google.common.truth.Expect;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.File;
import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
/**
@@ -51,184 +59,265 @@
*
* <p>Prefer adding new tests in CTS where possible.
*/
+@RunWith(Enclosed.class)
public class BrailleDisplayConnectionTest {
- private static final Path NULL_PATH = Path.of("/dev/null");
- private BrailleDisplayConnection mBrailleDisplayConnection;
- @Mock
- private BrailleDisplayConnection.NativeInterface mNativeInterface;
- @Mock
- private AccessibilityServiceConnection mServiceConnection;
+ public static class ScannerTest {
+ private static final Path NULL_PATH = Path.of("/dev/null");
- @Rule
- public final Expect expect = Expect.create();
+ private BrailleDisplayConnection mBrailleDisplayConnection;
+ @Mock
+ private BrailleDisplayConnection.NativeInterface mNativeInterface;
+ @Mock
+ private AccessibilityServiceConnection mServiceConnection;
- private Context mContext;
+ @Rule
+ public final Expect expect = Expect.create();
- // To mock package-private class
- @Rule
- public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
- new DexmakerShareClassLoaderRule();
+ private Context mContext;
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- mContext = InstrumentationRegistry.getInstrumentation().getContext();
- when(mServiceConnection.isConnectedLocked()).thenReturn(true);
- mBrailleDisplayConnection =
- spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
- }
+ // To mock package-private class
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
- @Test
- public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
- File testDir = mContext.getFilesDir();
- Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
- Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
- Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
- Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
- try {
- for (Path node : nodePaths) {
- assertThat(node.toFile().createNewFile()).isTrue();
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ when(mServiceConnection.isConnectedLocked()).thenReturn(true);
+ mBrailleDisplayConnection =
+ spy(new BrailleDisplayConnection(new Object(), mServiceConnection));
+ }
+
+ @Test
+ public void defaultNativeScanner_getHidrawNodePaths_returnsHidrawPaths() throws Exception {
+ File testDir = mContext.getFilesDir();
+ Path hidrawNode0 = Path.of(testDir.getPath(), "hidraw0");
+ Path hidrawNode1 = Path.of(testDir.getPath(), "hidraw1");
+ Path otherDevice = Path.of(testDir.getPath(), "otherDevice");
+ Path[] nodePaths = {hidrawNode0, hidrawNode1, otherDevice};
+ try {
+ for (Path node : nodePaths) {
+ assertThat(node.toFile().createNewFile()).isTrue();
+ }
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
+ .containsExactly(hidrawNode0, hidrawNode1);
+ } finally {
+ for (Path node : nodePaths) {
+ node.toFile().delete();
+ }
}
+ }
+
+ @Test
+ public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+ int descriptorSize = 4;
+ byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+ when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+ when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(
+ descriptor);
BrailleDisplayConnection.BrailleDisplayScanner scanner =
mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
- assertThat(scanner.getHidrawNodePaths(testDir.toPath()))
- .containsExactly(hidrawNode0, hidrawNode1);
- } finally {
- for (Path node : nodePaths) {
- node.toFile().delete();
- }
+ assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+ }
+
+ @Test
+ public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+ when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+ }
+
+ @Test
+ public void defaultNativeScanner_getUniqueId_returnsUniq() {
+ String macAddress = "12:34:56:78";
+ when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+ }
+
+ @Test
+ public void defaultNativeScanner_getDeviceBusType_busUsb() {
+ when(mNativeInterface.getHidrawBusType(anyInt()))
+ .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceBusType(NULL_PATH))
+ .isEqualTo(BrailleDisplayConnection.BUS_USB);
+ }
+
+ @Test
+ public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+ when(mNativeInterface.getHidrawBusType(anyInt()))
+ .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceBusType(NULL_PATH))
+ .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+ }
+
+ @Test
+ public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
+ Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
+ mBrailleDisplayConnection.write(
+ new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);
+
+ verify(mBrailleDisplayConnection).disconnect();
+ }
+
+ @Test
+ public void write_notConnected_throwsIllegalStateException() {
+ when(mServiceConnection.isConnectedLocked()).thenReturn(false);
+
+ assertThrows(IllegalStateException.class,
+ () -> mBrailleDisplayConnection.write(new byte[1]));
+ }
+
+ @Test
+ public void write_unableToCreateWriteStream_disconnects() {
+ Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
+ // mBrailleDisplayConnection#connectLocked was never called so the
+ // connection's mHidrawNode is still null. This will throw an exception
+ // when attempting to create FileOutputStream on the node.
+ mBrailleDisplayConnection.write(new byte[1]);
+
+ verify(mBrailleDisplayConnection).disconnect();
+ }
+
+ // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
+ // test Braille display data, but its own implementation should also be tested
+ // so that issues in this helper don't cause confusing failures in CTS.
+
+ @Test
+ public void setTestData_scannerReturnsTestData() {
+ Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+ Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+ bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+ path1.toString());
+ bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+ path2.toString());
+ byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+ bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+ bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+ String uniq1 = "uniq1", uniq2 = "uniq2";
+ bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+ bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+ int bus1 = BrailleDisplayConnection.BUS_USB, bus2 =
+ BrailleDisplayConnection.BUS_BLUETOOTH;
+ bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+ bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+ bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+ bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+ expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
+ expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+ expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+ expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+ expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+ expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+ expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+ }
+
+ @Test
+ public void setTestData_emptyTestData_returnsNullNodePaths() {
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.setTestData(List.of());
+
+ expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
}
}
- @Test
- public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
- int descriptorSize = 4;
- byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
- when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
- when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+ @RunWith(Parameterized.class)
+ public static class BrailleDisplayDescriptorTest {
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][]{
+ {"match_BdPage", new byte[]{
+ // Just one item, defines the BD page
+ 0x05, 0x41}},
+ {"match_BdPageAfterAnotherPage", new byte[]{
+ // One item defines another page
+ 0x05, 0x01,
+ // Next item defines BD page
+ 0x05, 0x41}},
+ {"match_BdPageAfterSizeZeroItem", new byte[]{
+ // Size-zero item (last 2 bits are 00)
+ 0x00,
+ // Next item defines BD page
+ 0x05, 0x41}},
+ {"match_BdPageAfterSizeOneItem", new byte[]{
+ // Size-one item (last 2 bits are 01)
+ 0x01, 0x7F,
+ // Next item defines BD page
+ 0x05, 0x41}},
+ {"match_BdPageAfterSizeTwoItem", new byte[]{
+ // Size-two item (last 2 bits are 10)
+ 0x02, 0x7F, 0x7F,
+ 0x05, 0x41}},
+ {"match_BdPageAfterSizeFourItem", new byte[]{
+ // Size-four item (last 2 bits are 11)
+ 0x03, 0x7F, 0x7F, 0x7F, 0x7F,
+ 0x05, 0x41}},
+ {"match_BdPageInBetweenOtherPages", new byte[]{
+ // One item defines another page
+ 0x05, 0x01,
+ // Next item defines BD page
+ 0x05, 0x41,
+ // Next item defines another page
+ 0x05, 0x02}},
+ {"fail_OtherPage", new byte[]{
+ // Just one item, defines another page
+ 0x05, 0x01}},
+ {"fail_BdPageBeforeMissingData", new byte[]{
+ // This item defines BD page
+ 0x05, 0x41,
+ // Next item specifies size-one item (last 2 bits are 01) but
+ // that one data byte is missing; this descriptor is malformed.
+ 0x01}},
+ {"fail_BdPageWithWrongDataSize", new byte[]{
+ // This item defines a page with two-byte ID 0x41 0x7F, not 0x41.
+ 0x06, 0x41, 0x7F}},
+ {"fail_LongItem", new byte[]{
+ // Item has type bits 1111, indicating Long Item.
+ (byte) 0xF0}},
+ });
+ }
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
- assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
- }
+ @Parameterized.Parameter(0)
+ public String mTestName;
+ @Parameterized.Parameter(1)
+ public byte[] mDescriptor;
- @Test
- public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
- when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
-
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
- assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
- }
-
- @Test
- public void defaultNativeScanner_getUniqueId_returnsUniq() {
- String macAddress = "12:34:56:78";
- when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
-
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
- assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
- }
-
- @Test
- public void defaultNativeScanner_getDeviceBusType_busUsb() {
- when(mNativeInterface.getHidrawBusType(anyInt()))
- .thenReturn(BrailleDisplayConnection.BUS_USB);
-
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
- assertThat(scanner.getDeviceBusType(NULL_PATH))
- .isEqualTo(BrailleDisplayConnection.BUS_USB);
- }
-
- @Test
- public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
- when(mNativeInterface.getHidrawBusType(anyInt()))
- .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
-
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
-
- assertThat(scanner.getDeviceBusType(NULL_PATH))
- .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
- }
-
- @Test
- public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
- Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
- mBrailleDisplayConnection.write(
- new byte[IBinder.getSuggestedMaxIpcSizeBytes() * 2]);
-
- verify(mBrailleDisplayConnection).disconnect();
- }
-
- @Test
- public void write_notConnected_throwsIllegalStateException() {
- when(mServiceConnection.isConnectedLocked()).thenReturn(false);
-
- assertThrows(IllegalStateException.class,
- () -> mBrailleDisplayConnection.write(new byte[1]));
- }
-
- @Test
- public void write_unableToCreateWriteStream_disconnects() {
- Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
- // mBrailleDisplayConnection#connectLocked was never called so the
- // connection's mHidrawNode is still null. This will throw an exception
- // when attempting to create FileOutputStream on the node.
- mBrailleDisplayConnection.write(new byte[1]);
-
- verify(mBrailleDisplayConnection).disconnect();
- }
-
- // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
- // test Braille display data, but its own implementation should also be tested
- // so that issues in this helper don't cause confusing failures in CTS.
-
- @Test
- public void setTestData_scannerReturnsTestData() {
- Bundle bd1 = new Bundle(), bd2 = new Bundle();
-
- Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
- bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
- bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
- byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
- bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
- bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
- String uniq1 = "uniq1", uniq2 = "uniq2";
- bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
- bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
- int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
- bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
- bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
- bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
- bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
-
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
-
- expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).containsExactly(path1, path2);
- expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
- expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
- expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
- expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
- expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
- expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
- }
-
- @Test
- public void setTestData_emptyTestData_returnsNullNodePaths() {
- BrailleDisplayConnection.BrailleDisplayScanner scanner =
- mBrailleDisplayConnection.setTestData(List.of());
-
- expect.that(scanner.getHidrawNodePaths(Path.of("/dev"))).isNull();
+ @Test
+ public void isBrailleDisplay() {
+ final boolean expectedMatch = mTestName.startsWith("match_");
+ assertWithMessage(
+ "Expected isBrailleDisplay==" + expectedMatch
+ + " for descriptor " + HexDump.toHexString(mDescriptor))
+ .that(BrailleDisplayConnection.isBrailleDisplay(mDescriptor))
+ .isEqualTo(expectedMatch);
+ }
}
}
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 81df597..3e748ff 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -75,6 +75,7 @@
import android.content.pm.SigningInfo;
import android.content.pm.UserInfo;
import android.content.pm.UserPackage;
+import android.content.pm.UserProperties;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.drawable.Icon;
@@ -776,6 +777,15 @@
new UserInfo(USER_P1, "userP1",
UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_MANAGED_PROFILE), 0);
+ protected static final UserProperties USER_PROPERTIES_0 =
+ new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build();
+
+ protected static final UserProperties USER_PROPERTIES_10 =
+ new UserProperties.Builder().setItemsRestrictedOnHomeScreen(false).build();
+
+ protected static final UserProperties USER_PROPERTIES_11 =
+ new UserProperties.Builder().setItemsRestrictedOnHomeScreen(true).build();
+
protected BiPredicate<String, Integer> mDefaultLauncherChecker =
(callingPackage, userId) ->
LAUNCHER_1.equals(callingPackage) || LAUNCHER_2.equals(callingPackage)
@@ -817,6 +827,7 @@
= new HashMap<>();
protected final Map<Integer, UserInfo> mUserInfos = new HashMap<>();
+ protected final Map<Integer, UserProperties> mUserProperties = new HashMap<>();
protected final Map<Integer, Boolean> mRunningUsers = new HashMap<>();
protected final Map<Integer, Boolean> mUnlockedUsers = new HashMap<>();
@@ -911,6 +922,9 @@
mUserInfos.put(USER_11, USER_INFO_11);
mUserInfos.put(USER_P0, USER_INFO_P0);
mUserInfos.put(USER_P1, USER_INFO_P1);
+ mUserProperties.put(USER_0, USER_PROPERTIES_0);
+ mUserProperties.put(USER_10, USER_PROPERTIES_10);
+ mUserProperties.put(USER_11, USER_PROPERTIES_11);
when(mMockUserManagerInternal.isUserUnlockingOrUnlocked(anyInt()))
.thenAnswer(inv -> {
@@ -959,6 +973,15 @@
inv -> mUserInfos.get((Integer) inv.getArguments()[0])));
when(mMockActivityManagerInternal.getUidProcessState(anyInt())).thenReturn(
ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+ when(mMockUserManagerInternal.getUserProperties(anyInt()))
+ .thenAnswer(inv -> {
+ final int userId = (Integer) inv.getArguments()[0];
+ final UserProperties userProperties = mUserProperties.get(userId);
+ if (userProperties == null) {
+ return new UserProperties.Builder().build();
+ }
+ return userProperties;
+ });
// User 0 and P0 are always running
mRunningUsers.put(USER_0, true);