Merge "Handle failures from Before/AfterClass in the runner" into main
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
index f20b170..3577fcd 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
@@ -194,7 +194,7 @@
/**
* Simple benchmark for the amount of time to send a given number of messages
*/
- @Test
+ // @Test Temporarily disabled
@Parameters(method = "getParams")
public void time(Config config) throws Exception {
reset();
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
index af3c405..ac57100 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
@@ -198,7 +198,7 @@
executor.awaitTermination(5, TimeUnit.SECONDS);
}
- @Test
+ // @Test Temporarily disabled
@Parameters(method = "getParams")
public void throughput(Config config) throws Exception {
setup(config);
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 80db264..5f55075 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -23,3 +23,10 @@
description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
bug: "318731461"
}
+
+flag {
+ name: "cleanup_empty_jobs"
+ namespace: "backstage_power"
+ description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis."
+ bug: "349688611"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index 96494ec..11d17ca 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -85,6 +85,14 @@
*/
@UnsupportedAppUsage
void jobFinished(int jobId, boolean reschedule);
+
+ /*
+ * Inform JobScheduler to force finish this job because the client has lost
+ * the job handle. jobFinished can no longer be called from the client.
+ * @param jobId Unique integer used to identify this job
+ */
+ void forceJobFinished(int jobId);
+
/*
* Inform JobScheduler of a change in the estimated transfer payload.
*
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index e833bb9..52a761f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -34,15 +34,21 @@
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.system.SystemCleaner;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Cleaner;
/**
* Contains the parameters used to configure/identify your job. You do not create this object
* yourself, instead it is handed in to your application by the System.
*/
public class JobParameters implements Parcelable {
+ private static final String TAG = "JobParameters";
/** @hide */
public static final int INTERNAL_STOP_REASON_UNKNOWN = -1;
@@ -306,6 +312,10 @@
private int mStopReason = STOP_REASON_UNDEFINED;
private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN;
private String debugStopReason; // Human readable stop reason for debugging.
+ @Nullable
+ private JobCleanupCallback mJobCleanupCallback;
+ @Nullable
+ private Cleaner.Cleanable mCleanable;
/** @hide */
public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
@@ -326,6 +336,8 @@
this.mTriggeredContentAuthorities = triggeredContentAuthorities;
this.mNetwork = network;
this.mJobNamespace = namespace;
+ this.mJobCleanupCallback = null;
+ this.mCleanable = null;
}
/**
@@ -597,6 +609,8 @@
mStopReason = in.readInt();
mInternalStopReason = in.readInt();
debugStopReason = in.readString();
+ mJobCleanupCallback = null;
+ mCleanable = null;
}
/** @hide */
@@ -612,6 +626,54 @@
this.debugStopReason = debugStopReason;
}
+ /** @hide */
+ public void initCleaner(JobCleanupCallback jobCleanupCallback) {
+ mJobCleanupCallback = jobCleanupCallback;
+ mCleanable = SystemCleaner.cleaner().register(this, mJobCleanupCallback);
+ }
+
+ /**
+ * Lazy initialize the cleaner and enable it
+ *
+ * @hide
+ */
+ public void enableCleaner() {
+ if (mJobCleanupCallback == null) {
+ initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId));
+ }
+ mJobCleanupCallback.enableCleaner();
+ }
+
+ /**
+ * Disable the cleaner from running and unregister it
+ *
+ * @hide
+ */
+ public void disableCleaner() {
+ if (mJobCleanupCallback != null) {
+ mJobCleanupCallback.disableCleaner();
+ if (mCleanable != null) {
+ mCleanable.clean();
+ mCleanable = null;
+ }
+ mJobCleanupCallback = null;
+ }
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ @Nullable
+ public Cleaner.Cleanable getCleanable() {
+ return mCleanable;
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ @Nullable
+ public JobCleanupCallback getJobCleanupCallback() {
+ return mJobCleanupCallback;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -647,6 +709,67 @@
dest.writeString(debugStopReason);
}
+ /**
+ * JobCleanupCallback is used track JobParameters leak. If the job is started
+ * and jobFinish is not called at the time of garbage collection of JobParameters
+ * instance, it is considered a job leak. Force finish the job.
+ *
+ * @hide
+ */
+ public static class JobCleanupCallback implements Runnable {
+ private final IJobCallback mCallback;
+ private final int mJobId;
+ private boolean mIsCleanerEnabled;
+
+ public JobCleanupCallback(
+ IJobCallback callback,
+ int jobId) {
+ mCallback = callback;
+ mJobId = jobId;
+ mIsCleanerEnabled = false;
+ }
+
+ /**
+ * Check if the cleaner is enabled
+ *
+ * @hide
+ */
+ public boolean isCleanerEnabled() {
+ return mIsCleanerEnabled;
+ }
+
+ /**
+ * Enable the cleaner to detect JobParameter leak
+ *
+ * @hide
+ */
+ public void enableCleaner() {
+ mIsCleanerEnabled = true;
+ }
+
+ /**
+ * Disable the cleaner from running.
+ *
+ * @hide
+ */
+ public void disableCleaner() {
+ mIsCleanerEnabled = false;
+ }
+
+ /** @hide */
+ @Override
+ public void run() {
+ if (!isCleanerEnabled()) {
+ return;
+ }
+ try {
+ mCallback.forceJobFinished(mJobId);
+ } catch (Exception e) {
+ Log.wtf(TAG, "Could not destroy running job", e);
+ }
+ }
+ }
+
public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
@Override
public JobParameters createFromParcel(Parcel in) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 79d87ed..5f80c52 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -165,7 +165,13 @@
case MSG_EXECUTE_JOB: {
final JobParameters params = (JobParameters) msg.obj;
try {
+ if (Flags.cleanupEmptyJobs()) {
+ params.enableCleaner();
+ }
boolean workOngoing = JobServiceEngine.this.onStartJob(params);
+ if (Flags.cleanupEmptyJobs() && !workOngoing) {
+ params.disableCleaner();
+ }
ackStartMessage(params, workOngoing);
} catch (Exception e) {
Log.e(TAG, "Error while executing job: " + params.getJobId());
@@ -190,6 +196,9 @@
IJobCallback callback = params.getCallback();
if (callback != null) {
try {
+ if (Flags.cleanupEmptyJobs()) {
+ params.disableCleaner();
+ }
callback.jobFinished(params.getJobId(), needsReschedule);
} catch (RemoteException e) {
Log.e(TAG, "Error reporting job finish to system: binder has gone" +
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index be8e304..ee246d8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -129,6 +129,8 @@
private static final String[] VERB_STRINGS = {
"VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
};
+ private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:";
+ private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#";
// States that a job occupies while interacting with the client.
static final int VERB_BINDING = 0;
@@ -292,6 +294,11 @@
}
@Override
+ public void forceJobFinished(int jobId) {
+ doForceJobFinished(this, jobId);
+ }
+
+ @Override
public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
long downloadBytes, long uploadBytes) {
doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
@@ -762,6 +769,35 @@
}
}
+ /**
+ * This method just adds traces to evaluate jobs that leak jobparameters at the client.
+ * It does not stop the job.
+ */
+ void doForceJobFinished(JobCallback cb, int jobId) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final JobStatus executing;
+ synchronized (mLock) {
+ // not the current job, presumably it has finished in some way already
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+
+ executing = getRunningJobLocked();
+ }
+ if (executing != null && jobId == executing.getJobId()) {
+ final StringBuilder stateSuffix = new StringBuilder();
+ stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX);
+ stateSuffix.append(executing.getBatteryName());
+ stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER);
+ stateSuffix.append(executing.getJobId());
+ Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString());
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId,
int workId, @BytesLong long transferredBytes) {
// TODO(255393346): Make sure apps call this appropriately and monitor for abuse
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b83be6b..b4fb480 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -88,6 +88,7 @@
import android.view.WindowInsetsController.Appearance;
import android.window.TaskSnapshot;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.LocalePicker;
import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.RoSystemProperties;
@@ -238,6 +239,14 @@
private static final RateLimitingCache<List<ProcessErrorStateInfo>> mErrorProcessesCache =
new RateLimitingCache<>(10, 2);
+ /** Rate-Limiting cache that allows no more than 100 calls to the service per second. */
+ @GuardedBy("mMemoryInfoCache")
+ private static final RateLimitingCache<MemoryInfo> mMemoryInfoCache =
+ new RateLimitingCache<>(10);
+ /** Used to store cached results for rate-limited calls to getMemoryInfo(). */
+ @GuardedBy("mMemoryInfoCache")
+ private static final MemoryInfo mRateLimitedMemInfo = new MemoryInfo();
+
/**
* Query handler for mGetCurrentUserIdCache - returns a cached value of the current foreground
* user id if the backstage_power/android.app.cache_get_current_user_id flag is enabled.
@@ -3510,6 +3519,19 @@
foregroundAppThreshold = source.readLong();
}
+ /** @hide */
+ public void copyTo(MemoryInfo other) {
+ other.advertisedMem = advertisedMem;
+ other.availMem = availMem;
+ other.totalMem = totalMem;
+ other.threshold = threshold;
+ other.lowMemory = lowMemory;
+ other.hiddenAppThreshold = hiddenAppThreshold;
+ other.secondaryServerThreshold = secondaryServerThreshold;
+ other.visibleAppThreshold = visibleAppThreshold;
+ other.foregroundAppThreshold = foregroundAppThreshold;
+ }
+
public static final @android.annotation.NonNull Creator<MemoryInfo> CREATOR
= new Creator<MemoryInfo>() {
public MemoryInfo createFromParcel(Parcel source) {
@@ -3536,6 +3558,20 @@
* manage its memory.
*/
public void getMemoryInfo(MemoryInfo outInfo) {
+ if (Flags.rateLimitGetMemoryInfo()) {
+ synchronized (mMemoryInfoCache) {
+ mMemoryInfoCache.get(() -> {
+ getMemoryInfoInternal(mRateLimitedMemInfo);
+ return mRateLimitedMemInfo;
+ });
+ mRateLimitedMemInfo.copyTo(outInfo);
+ }
+ } else {
+ getMemoryInfoInternal(outInfo);
+ }
+ }
+
+ private void getMemoryInfoInternal(MemoryInfo outInfo) {
try {
getService().getMemoryInfo(outInfo);
} catch (RemoteException e) {
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 4d61f41..c0c81df 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -125,3 +125,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "rate_limit_get_memory_info"
+ description: "Rate limit calls to getMemoryInfo using a cache"
+ is_fixed_read_only: true
+ bug: "364312431"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index da3cc1b..031380d 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -86,6 +86,7 @@
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.XmlUtils;
+import com.android.modules.expresslog.Counter;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -12805,6 +12806,8 @@
new ClipData.Item(text, htmlText, null, stream));
setClipData(clipData);
if (stream != null) {
+ logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_send_action");
addFlags(FLAG_GRANT_READ_URI_PERMISSION);
}
return true;
@@ -12846,6 +12849,8 @@
setClipData(clipData);
if (streams != null) {
+ logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_send_multiple_action");
addFlags(FLAG_GRANT_READ_URI_PERMISSION);
}
return true;
@@ -12865,6 +12870,10 @@
putExtra(MediaStore.EXTRA_OUTPUT, output);
setClipData(ClipData.newRawUri("", output));
+
+ logCounterIfFlagsMissing(
+ FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_READ_URI_PERMISSION,
+ "intents.value_explicit_uri_grant_for_image_capture_action");
addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
return true;
}
@@ -12873,6 +12882,12 @@
return false;
}
+ private void logCounterIfFlagsMissing(int requiredFlags, String metricId) {
+ if ((getFlags() & requiredFlags) != requiredFlags) {
+ Counter.logIncrement(metricId);
+ }
+ }
+
@android.ravenwood.annotation.RavenwoodThrow
private Uri maybeConvertFileToContentUri(Context context, Uri uri) {
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index 6514872..ef59e0a 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -26,6 +26,10 @@
import android.database.sqlite.SQLiteException;
import android.os.Parcel;
import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodThrow;
import dalvik.annotation.optimization.FastNative;
import dalvik.system.CloseGuard;
@@ -40,9 +44,8 @@
* consumer for reading.
* </p>
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.CursorWindow_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("CursorWindow_host")
public class CursorWindow extends SQLiteClosable implements Parcelable {
private static final String STATS_TAG = "CursorWindowStats";
@@ -63,48 +66,69 @@
private final CloseGuard mCloseGuard;
// May throw CursorWindowAllocationException
+ @RavenwoodRedirect
private static native long nativeCreate(String name, int cursorWindowSize);
// May throw CursorWindowAllocationException
+ @RavenwoodRedirect
private static native long nativeCreateFromParcel(Parcel parcel);
+ @RavenwoodRedirect
private static native void nativeDispose(long windowPtr);
+ @RavenwoodRedirect
private static native void nativeWriteToParcel(long windowPtr, Parcel parcel);
+ @RavenwoodRedirect
private static native String nativeGetName(long windowPtr);
+ @RavenwoodRedirect
private static native byte[] nativeGetBlob(long windowPtr, int row, int column);
+ @RavenwoodRedirect
private static native String nativeGetString(long windowPtr, int row, int column);
+ @RavenwoodThrow
private static native void nativeCopyStringToBuffer(long windowPtr, int row, int column,
CharArrayBuffer buffer);
+ @RavenwoodRedirect
private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column);
+ @RavenwoodRedirect
private static native boolean nativePutString(long windowPtr, String value,
int row, int column);
// Below native methods don't do unconstrained work, so are FastNative for performance
@FastNative
+ @RavenwoodThrow
private static native void nativeClear(long windowPtr);
@FastNative
+ @RavenwoodRedirect
private static native int nativeGetNumRows(long windowPtr);
@FastNative
+ @RavenwoodRedirect
private static native boolean nativeSetNumColumns(long windowPtr, int columnNum);
@FastNative
+ @RavenwoodRedirect
private static native boolean nativeAllocRow(long windowPtr);
@FastNative
+ @RavenwoodThrow
private static native void nativeFreeLastRow(long windowPtr);
@FastNative
+ @RavenwoodRedirect
private static native int nativeGetType(long windowPtr, int row, int column);
@FastNative
+ @RavenwoodRedirect
private static native long nativeGetLong(long windowPtr, int row, int column);
@FastNative
+ @RavenwoodRedirect
private static native double nativeGetDouble(long windowPtr, int row, int column);
@FastNative
+ @RavenwoodRedirect
private static native boolean nativePutLong(long windowPtr, long value, int row, int column);
@FastNative
+ @RavenwoodRedirect
private static native boolean nativePutDouble(long windowPtr, double value, int row, int column);
@FastNative
+ @RavenwoodThrow
private static native boolean nativePutNull(long windowPtr, int row, int column);
diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java
index 07fbe4a..0541a96 100644
--- a/core/java/android/os/AppZygote.java
+++ b/core/java/android/os/AppZygote.java
@@ -111,12 +111,15 @@
try {
int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote(
mAppInfo, mProcessInfo);
+
+ final int[] sharedAppGid = {
+ UserHandle.getSharedAppGid(UserHandle.getAppId(mAppInfo.uid)) };
mZygote = Process.ZYGOTE_PROCESS.startChildZygote(
"com.android.internal.os.AppZygoteInit",
mAppInfo.processName + "_zygote",
mZygoteUid,
mZygoteUid,
- null, // gids
+ sharedAppGid, // Zygote gets access to shared app GID for profiles
runtimeFlags,
"app_zygote", // seInfo
abi, // abi
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index da2eec9..b2d9260 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -19,9 +19,9 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.TestApi;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;
import android.util.SparseArray;
@@ -51,9 +51,8 @@
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
public final class MessageQueue {
private static final String TAG = "ConcurrentMessageQueue";
private static final boolean DEBUG = false;
@@ -345,11 +344,17 @@
// Barriers are indicated by messages with a null target whose arg1 field carries the token.
private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
+ @RavenwoodRedirect
private static native long nativeInit();
+ @RavenwoodRedirect
private static native void nativeDestroy(long ptr);
+ @RavenwoodRedirect
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @RavenwoodRedirect
private static native void nativeWake(long ptr);
+ @RavenwoodRedirect
private static native boolean nativeIsPolling(long ptr);
+ @RavenwoodRedirect
private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 6b9b349..4474e7e 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -20,9 +20,9 @@
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Handler;
-import android.os.Process;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;
import android.util.SparseArray;
@@ -42,9 +42,8 @@
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
public final class MessageQueue {
private static final String TAG = "MessageQueue";
private static final boolean DEBUG = false;
@@ -79,12 +78,18 @@
@UnsupportedAppUsage
private int mNextBarrierToken;
+ @RavenwoodRedirect
private native static long nativeInit();
+ @RavenwoodRedirect
private native static void nativeDestroy(long ptr);
@UnsupportedAppUsage
+ @RavenwoodRedirect
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @RavenwoodRedirect
private native static void nativeWake(long ptr);
+ @RavenwoodRedirect
private native static boolean nativeIsPolling(long ptr);
+ @RavenwoodRedirect
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
index b24e14b..f1affce 100644
--- a/core/java/android/os/LockedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -20,8 +20,9 @@
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Handler;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;
import android.util.SparseArray;
@@ -44,9 +45,8 @@
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
public final class MessageQueue {
private static final String TAG = "LockedMessageQueue";
private static final boolean DEBUG = false;
@@ -389,12 +389,18 @@
@UnsupportedAppUsage
private int mNextBarrierToken;
+ @RavenwoodRedirect
private native static long nativeInit();
+ @RavenwoodRedirect
private native static void nativeDestroy(long ptr);
@UnsupportedAppUsage
+ @RavenwoodRedirect
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @RavenwoodRedirect
private native static void nativeWake(long ptr);
+ @RavenwoodRedirect
private native static boolean nativeIsPolling(long ptr);
+ @RavenwoodRedirect
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 47096db..2ac2ae9 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -28,7 +28,8 @@
import android.app.AppOpsManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.ravenwood.annotation.RavenwoodReplace;
import android.ravenwood.annotation.RavenwoodThrow;
import android.text.TextUtils;
@@ -233,8 +234,7 @@
* {@link #readSparseArray(ClassLoader, Class)}.
*/
@RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.Parcel_host")
+@RavenwoodRedirectionClass("Parcel_host")
public final class Parcel {
private static final boolean DEBUG_RECYCLE = false;
@@ -387,6 +387,7 @@
private static final int SIZE_COMPLEX_TYPE = 1;
@CriticalNative
+ @RavenwoodRedirect
private static native void nativeMarkSensitive(long nativePtr);
@FastNative
@RavenwoodThrow
@@ -395,86 +396,126 @@
@RavenwoodThrow
private static native boolean nativeIsForRpc(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeDataSize(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeDataAvail(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeDataPosition(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeDataCapacity(long nativePtr);
@FastNative
+ @RavenwoodRedirect
private static native void nativeSetDataSize(long nativePtr, int size);
@CriticalNative
+ @RavenwoodRedirect
private static native void nativeSetDataPosition(long nativePtr, int pos);
@FastNative
+ @RavenwoodRedirect
private static native void nativeSetDataCapacity(long nativePtr, int size);
@CriticalNative
+ @RavenwoodRedirect
private static native boolean nativePushAllowFds(long nativePtr, boolean allowFds);
@CriticalNative
+ @RavenwoodRedirect
private static native void nativeRestoreAllowFds(long nativePtr, boolean lastValue);
+ @RavenwoodRedirect
private static native void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len);
+ @RavenwoodRedirect
private static native void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeWriteInt(long nativePtr, int val);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeWriteLong(long nativePtr, long val);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeWriteFloat(long nativePtr, float val);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeWriteDouble(long nativePtr, double val);
@RavenwoodThrow
private static native void nativeSignalExceptionForError(int error);
@FastNative
+ @RavenwoodRedirect
private static native void nativeWriteString8(long nativePtr, String val);
@FastNative
+ @RavenwoodRedirect
private static native void nativeWriteString16(long nativePtr, String val);
@FastNative
@RavenwoodThrow
private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
@FastNative
+ @RavenwoodRedirect
private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
+ @RavenwoodRedirect
private static native byte[] nativeCreateByteArray(long nativePtr);
+ @RavenwoodRedirect
private static native boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen);
+ @RavenwoodRedirect
private static native byte[] nativeReadBlob(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native int nativeReadInt(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native long nativeReadLong(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native float nativeReadFloat(long nativePtr);
@CriticalNative
+ @RavenwoodRedirect
private static native double nativeReadDouble(long nativePtr);
@FastNative
+ @RavenwoodRedirect
private static native String nativeReadString8(long nativePtr);
@FastNative
+ @RavenwoodRedirect
private static native String nativeReadString16(long nativePtr);
@FastNative
@RavenwoodThrow
private static native IBinder nativeReadStrongBinder(long nativePtr);
@FastNative
+ @RavenwoodRedirect
private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
+ @RavenwoodRedirect
private static native long nativeCreate();
+ @RavenwoodRedirect
private static native void nativeFreeBuffer(long nativePtr);
+ @RavenwoodRedirect
private static native void nativeDestroy(long nativePtr);
+ @RavenwoodRedirect
private static native byte[] nativeMarshall(long nativePtr);
+ @RavenwoodRedirect
private static native void nativeUnmarshall(
long nativePtr, byte[] data, int offset, int length);
+ @RavenwoodRedirect
private static native int nativeCompareData(long thisNativePtr, long otherNativePtr);
+ @RavenwoodRedirect
private static native boolean nativeCompareDataInRange(
long ptrA, int offsetA, long ptrB, int offsetB, int length);
+ @RavenwoodRedirect
private static native void nativeAppendFrom(
long thisNativePtr, long otherNativePtr, int offset, int length);
@CriticalNative
+ @RavenwoodRedirect
private static native boolean nativeHasFileDescriptors(long nativePtr);
+ @RavenwoodRedirect
private static native boolean nativeHasFileDescriptorsInRange(
long nativePtr, int offset, int length);
+ @RavenwoodRedirect
private static native boolean nativeHasBinders(long nativePtr);
+ @RavenwoodRedirect
private static native boolean nativeHasBindersInRange(
long nativePtr, int offset, int length);
@RavenwoodThrow
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 79f229a..80c24a9 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -19,8 +19,9 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.TestApi;
-import android.os.Handler;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.Printer;
import android.util.SparseArray;
@@ -37,8 +38,6 @@
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.PriorityQueue;
-import java.util.PriorityQueue;
-import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -50,9 +49,8 @@
* <p>You can retrieve the MessageQueue for the current thread with
* {@link Looper#myQueue() Looper.myQueue()}.
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
public final class MessageQueue {
private static final String TAG = "SemiConcurrentMessageQueue";
private static final boolean DEBUG = false;
@@ -338,11 +336,17 @@
// Barriers are indicated by messages with a null target whose arg1 field carries the token.
private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
+ @RavenwoodRedirect
private static native long nativeInit();
+ @RavenwoodRedirect
private static native void nativeDestroy(long ptr);
+ @RavenwoodRedirect
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @RavenwoodRedirect
private static native void nativeWake(long ptr);
+ @RavenwoodRedirect
private static native boolean nativeIsPolling(long ptr);
+ @RavenwoodRedirect
private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
index 0a38691..e53873b 100644
--- a/core/java/android/os/SystemProperties.java
+++ b/core/java/android/os/SystemProperties.java
@@ -21,11 +21,13 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.util.Log;
import android.util.MutableInt;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.ravenwood.RavenwoodEnvironment;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -56,8 +58,7 @@
*/
@SystemApi
@RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.SystemProperties_host")
+@RavenwoodRedirectionClass("SystemProperties_host")
public class SystemProperties {
private static final String TAG = "SystemProperties";
private static final boolean TRACK_KEY_ACCESS = false;
@@ -75,7 +76,7 @@
@UnsupportedAppUsage
@GuardedBy("sChangeCallbacks")
- private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
+ static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
@GuardedBy("sRoReads")
private static final HashMap<String, MutableInt> sRoReads =
@@ -102,30 +103,18 @@
}
/** @hide */
+ @RavenwoodRedirect
public static void init$ravenwood(Map<String, String> values,
Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
- native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
- SystemProperties::callChangeCallbacks);
- synchronized (sChangeCallbacks) {
- sChangeCallbacks.clear();
- }
+ throw RavenwoodEnvironment.notSupportedOnDevice();
}
/** @hide */
+ @RavenwoodRedirect
public static void reset$ravenwood() {
- native_reset$ravenwood();
- synchronized (sChangeCallbacks) {
- sChangeCallbacks.clear();
- }
+ throw RavenwoodEnvironment.notSupportedOnDevice();
}
- // These native methods are currently only implemented by Ravenwood, as it's the only
- // mechanism we have to jump to our RavenwoodNativeSubstitutionClass
- private static native void native_init$ravenwood(Map<String, String> values,
- Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
- Runnable changeCallback);
- private static native void native_reset$ravenwood();
-
// The one-argument version of native_get used to be a regular native function. Nowadays,
// we use the two-argument form of native_get all the time, but we can't just delete the
// one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation
@@ -137,34 +126,46 @@
@FastNative
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @RavenwoodRedirect
private static native String native_get(String key, String def);
@FastNative
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @RavenwoodRedirect
private static native int native_get_int(String key, int def);
@FastNative
@UnsupportedAppUsage
+ @RavenwoodRedirect
private static native long native_get_long(String key, long def);
@FastNative
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @RavenwoodRedirect
private static native boolean native_get_boolean(String key, boolean def);
@FastNative
+ @RavenwoodRedirect
private static native long native_find(String name);
@FastNative
+ @RavenwoodRedirect
private static native String native_get(long handle);
@CriticalNative
+ @RavenwoodRedirect
private static native int native_get_int(long handle, int def);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_get_long(long handle, long def);
@CriticalNative
+ @RavenwoodRedirect
private static native boolean native_get_boolean(long handle, boolean def);
// _NOT_ FastNative: native_set performs IPC and can block
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @RavenwoodRedirect
private static native void native_set(String key, String def);
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+ @RavenwoodRedirect
private static native void native_add_change_callback();
+ @RavenwoodRedirect
private static native void native_report_sysprop_change();
/**
@@ -300,7 +301,7 @@
}
@SuppressWarnings("unused") // Called from native code.
- private static void callChangeCallbacks() {
+ static void callChangeCallbacks() {
ArrayList<Runnable> callbacks = null;
synchronized (sChangeCallbacks) {
//Log.i("foo", "Calling " + sChangeCallbacks.size() + " change callbacks!");
diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java
index 0a73fd1..00545da 100644
--- a/core/java/android/util/EventLog.java
+++ b/core/java/android/util/EventLog.java
@@ -21,6 +21,10 @@
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodThrow;
import java.io.BufferedReader;
import java.io.FileReader;
@@ -48,9 +52,8 @@
* They carry a payload of one or more int, long, or String values. The
* event-log-tags file defines the payload contents for each type code.
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.EventLog_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("EventLog_host")
public class EventLog {
/** @hide */ public EventLog() {}
@@ -339,6 +342,7 @@
* @param value A value to log
* @return The number of bytes written
*/
+ @RavenwoodRedirect
public static native int writeEvent(int tag, int value);
/**
@@ -347,6 +351,7 @@
* @param value A value to log
* @return The number of bytes written
*/
+ @RavenwoodRedirect
public static native int writeEvent(int tag, long value);
/**
@@ -355,6 +360,7 @@
* @param value A value to log
* @return The number of bytes written
*/
+ @RavenwoodRedirect
public static native int writeEvent(int tag, float value);
/**
@@ -363,6 +369,7 @@
* @param str A value to log
* @return The number of bytes written
*/
+ @RavenwoodRedirect
public static native int writeEvent(int tag, String str);
/**
@@ -371,6 +378,7 @@
* @param list A list of values to log
* @return The number of bytes written
*/
+ @RavenwoodRedirect
public static native int writeEvent(int tag, Object... list);
/**
@@ -379,6 +387,7 @@
* @param output container to add events into
* @throws IOException if something goes wrong reading events
*/
+ @RavenwoodThrow
public static native void readEvents(int[] tags, Collection<Event> output)
throws IOException;
@@ -391,6 +400,7 @@
* @hide
*/
@SystemApi
+ @RavenwoodThrow
public static native void readEventsOnWrapping(int[] tags, long timestamp,
Collection<Event> output)
throws IOException;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f021bdf..e10cc28 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4345,6 +4345,7 @@
handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
mPendingTransaction, "view not visible");
+ mHasPendingTransactions = false;
} else if (cancelAndRedraw) {
if (!mWasLastDrawCanceled) {
logAndTrace("Canceling draw."
@@ -4372,6 +4373,7 @@
if (!performDraw(mActiveSurfaceSyncGroup)) {
handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
mPendingTransaction, mLastPerformDrawSkippedReason);
+ mHasPendingTransactions = false;
}
}
mWasLastDrawCanceled = cancelAndRedraw;
@@ -4388,7 +4390,14 @@
mReportNextDraw = false;
mLastReportNextDrawReason = null;
mActiveSurfaceSyncGroup = null;
- mHasPendingTransactions = false;
+ if (mHasPendingTransactions) {
+ // TODO: We shouldn't ever actually hit this, it means mPendingTransaction wasn't
+ // merged with a sync group or BLASTBufferQueue before making it to this point
+ // But better a one or two frame flicker than steady-state broken from dropping
+ // whatever is in this transaction
+ mPendingTransaction.apply();
+ mHasPendingTransactions = false;
+ }
mSyncBuffer = false;
if (isInWMSRequestedSync()) {
mWmsRequestSyncGroup.markSyncReady();
@@ -5305,6 +5314,7 @@
private void registerCallbackForPendingTransactions() {
Transaction t = new Transaction();
t.merge(mPendingTransaction);
+ mHasPendingTransactions = false;
registerRtFrameCallback(new FrameDrawingCallback() {
@Override
@@ -5384,6 +5394,7 @@
if (!usingAsyncReport && mHasPendingTransactions) {
pendingTransaction = new Transaction();
pendingTransaction.merge(mPendingTransaction);
+ mHasPendingTransactions = false;
} else {
pendingTransaction = null;
}
@@ -9942,6 +9953,7 @@
}
handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
mPendingTransaction, "shutting down VRI");
+ mHasPendingTransactions = false;
WindowManagerGlobal.getInstance().doRemoveView(this);
}
@@ -12601,6 +12613,7 @@
if (mHasPendingTransactions) {
t = new Transaction();
t.merge(mPendingTransaction);
+ mHasPendingTransactions = false;
} else {
t = null;
}
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 4cc0d8a..c3168001 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -69,6 +69,23 @@
public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";
/**
+ * Key to bundle {@link TaskFragmentInfo}s from the system in
+ * {@link #registerOrganizer(boolean, Bundle)}
+ *
+ * @hide
+ */
+ public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info";
+
+ /**
+ * Key to bundle {@link TaskFragmentParentInfo} from the system in
+ * {@link #registerOrganizer(boolean, Bundle)}
+ *
+ * @hide
+ */
+ public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO =
+ "key_restore_task_fragment_parent_info";
+
+ /**
* No change set.
*/
@WindowManager.TransitionType
diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
index 07fa679..dfb2884 100644
--- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
@@ -18,6 +18,10 @@
import android.os.Parcel;
import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodReplace;
import com.android.internal.util.Preconditions;
@@ -55,18 +59,15 @@
*
* @hide
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.LongArrayMultiStateCounter_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("LongArrayMultiStateCounter_host")
public final class LongArrayMultiStateCounter implements Parcelable {
/**
* Container for a native equivalent of a long[].
*/
- @android.ravenwood.annotation.RavenwoodKeepWholeClass
- @android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution"
- + ".LongArrayMultiStateCounter_host$LongArrayContainer_host")
+ @RavenwoodKeepWholeClass
+ @RavenwoodRedirectionClass("LongArrayContainer_host")
public static class LongArrayContainer {
private static NativeAllocationRegistry sRegistry;
@@ -81,7 +82,7 @@
registerNativeAllocation();
}
- @android.ravenwood.annotation.RavenwoodReplace
+ @RavenwoodReplace
private void registerNativeAllocation() {
if (sRegistry == null) {
synchronized (LongArrayMultiStateCounter.class) {
@@ -140,18 +141,23 @@
}
@CriticalNative
+ @RavenwoodRedirect
private static native long native_init(int length);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_getReleaseFunc();
@FastNative
+ @RavenwoodRedirect
private static native void native_setValues(long nativeObject, long[] array);
@FastNative
+ @RavenwoodRedirect
private static native void native_getValues(long nativeObject, long[] array);
@FastNative
+ @RavenwoodRedirect
private static native boolean native_combineValues(long nativeObject, long[] array,
int[] indexMap);
}
@@ -175,7 +181,7 @@
registerNativeAllocation();
}
- @android.ravenwood.annotation.RavenwoodReplace
+ @RavenwoodReplace
private void registerNativeAllocation() {
if (sRegistry == null) {
synchronized (LongArrayMultiStateCounter.class) {
@@ -374,57 +380,73 @@
@CriticalNative
+ @RavenwoodRedirect
private static native long native_init(int stateCount, int arrayLength);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_getReleaseFunc();
@CriticalNative
+ @RavenwoodRedirect
private static native void native_setEnabled(long nativeObject, boolean enabled,
long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_setState(long nativeObject, int state, long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_copyStatesFrom(long nativeObjectTarget,
long nativeObjectSource);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_setValues(long nativeObject, int state,
long longArrayContainerNativeObject);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_updateValues(long nativeObject,
long longArrayContainerNativeObject, long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_incrementValues(long nativeObject,
long longArrayContainerNativeObject, long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_addCounts(long nativeObject,
long longArrayContainerNativeObject);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_reset(long nativeObject);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_getCounts(long nativeObject,
long longArrayContainerNativeObject, int state);
@FastNative
+ @RavenwoodRedirect
private static native String native_toString(long nativeObject);
@FastNative
+ @RavenwoodRedirect
private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
@FastNative
+ @RavenwoodRedirect
private static native long native_initFromParcel(Parcel parcel);
@CriticalNative
+ @RavenwoodRedirect
private static native int native_getStateCount(long nativeObject);
@CriticalNative
+ @RavenwoodRedirect
private static native int native_getArrayLength(long nativeObject);
}
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
index e5662c7..c386a86 100644
--- a/core/java/com/android/internal/os/LongMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -18,6 +18,10 @@
import android.os.Parcel;
import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodReplace;
import com.android.internal.util.Preconditions;
@@ -55,9 +59,8 @@
*
* @hide
*/
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.LongMultiStateCounter_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("LongMultiStateCounter_host")
public final class LongMultiStateCounter implements Parcelable {
private static NativeAllocationRegistry sRegistry;
@@ -82,7 +85,7 @@
mStateCount = native_getStateCount(mNativeObject);
}
- @android.ravenwood.annotation.RavenwoodReplace
+ @RavenwoodReplace
private void registerNativeAllocation() {
if (sRegistry == null) {
synchronized (LongMultiStateCounter.class) {
@@ -210,43 +213,56 @@
@CriticalNative
+ @RavenwoodRedirect
private static native long native_init(int stateCount);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_getReleaseFunc();
@CriticalNative
+ @RavenwoodRedirect
private static native void native_setEnabled(long nativeObject, boolean enabled,
long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_setState(long nativeObject, int state, long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_updateValue(long nativeObject, long value, long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_incrementValue(long nativeObject, long increment,
long timestampMs);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_addCount(long nativeObject, long count);
@CriticalNative
+ @RavenwoodRedirect
private static native void native_reset(long nativeObject);
@CriticalNative
+ @RavenwoodRedirect
private static native long native_getCount(long nativeObject, int state);
@FastNative
+ @RavenwoodRedirect
private static native String native_toString(long nativeObject);
@FastNative
+ @RavenwoodRedirect
private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
@FastNative
+ @RavenwoodRedirect
private static native long native_initFromParcel(Parcel parcel);
@CriticalNative
+ @RavenwoodRedirect
private static native int native_getStateCount(long nativeObject);
}
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 12d3264..032ac42 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -3025,6 +3025,7 @@
@Override
public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) {
this.splitCodePaths = splitCodePaths;
+ this.mSplits = null; // reset for paths changed
if (splitCodePaths != null) {
int size = splitCodePaths.length;
for (int index = 0; index < size; index++) {
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 319efe0..30b160a 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -16,15 +16,15 @@
package com.android.internal.ravenwood;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
import android.ravenwood.annotation.RavenwoodReplace;
/**
* Class to interact with the Ravenwood environment.
*/
@RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
- "com.android.platform.test.ravenwood.nativesubstitution.RavenwoodEnvironment_host")
+@RavenwoodRedirectionClass("RavenwoodEnvironment_host")
public final class RavenwoodEnvironment {
public static final String TAG = "RavenwoodEnvironment";
@@ -40,7 +40,7 @@
ensureRavenwoodInitialized();
}
- private static RuntimeException notSupportedOnDevice() {
+ public static RuntimeException notSupportedOnDevice() {
return new UnsupportedOperationException("This method can only be used on Ravenwood");
}
@@ -56,14 +56,10 @@
*
* No-op if called on the device side.
*/
- @RavenwoodReplace
+ @RavenwoodRedirect
public static void ensureRavenwoodInitialized() {
}
- private static void ensureRavenwoodInitialized$ravenwood() {
- nativeEnsureRavenwoodInitialized();
- }
-
/**
* USE IT SPARINGLY! Returns true if it's running on Ravenwood, hostside test environment.
*
@@ -89,15 +85,11 @@
* Get the object back from the address obtained from
* {@link dalvik.system.VMRuntime#addressOf(Object)}.
*/
- @RavenwoodReplace
+ @RavenwoodRedirect
public <T> T fromAddress(long address) {
throw notSupportedOnDevice();
}
- private <T> T fromAddress$ravenwood(long address) {
- return nativeFromAddress(address);
- }
-
/**
* See {@link Workaround}. It's only usable on Ravenwood.
*/
@@ -113,20 +105,11 @@
/**
* @return the "ravenwood-runtime" directory.
*/
- @RavenwoodReplace
+ @RavenwoodRedirect
public String getRavenwoodRuntimePath() {
throw notSupportedOnDevice();
}
- private String getRavenwoodRuntimePath$ravenwood() {
- return nativeGetRavenwoodRuntimePath();
- }
-
- // Private native methods that are actually substituted on Ravenwood
- private native <T> T nativeFromAddress(long address);
- private native String nativeGetRavenwoodRuntimePath();
- private static native void nativeEnsureRavenwoodInitialized();
-
/**
* A set of APIs used to work around missing features on Ravenwood. Ideally, this class should
* be empty, and all its APIs should be able to be implemented properly.
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 11c220b..0ec55f9 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -120,6 +120,7 @@
private static final String TAG = "LockPatternView";
private OnPatternListener mOnPatternListener;
+ private ExternalHapticsPlayer mExternalHapticsPlayer;
@UnsupportedAppUsage
private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
@@ -317,6 +318,13 @@
void onPatternDetected(List<Cell> pattern);
}
+ /** An external haptics player for pattern updates. */
+ public interface ExternalHapticsPlayer{
+
+ /** Perform haptic feedback when a cell is added to the pattern. */
+ void performCellAddedFeedback();
+ }
+
public LockPatternView(Context context) {
this(context, null);
}
@@ -461,6 +469,15 @@
}
/**
+ * Set the external haptics player for feedback on pattern detection.
+ * @param player The external player.
+ */
+ @UnsupportedAppUsage
+ public void setExternalHapticsPlayer(ExternalHapticsPlayer player) {
+ mExternalHapticsPlayer = player;
+ }
+
+ /**
* Set the pattern explicitely (rather than waiting for the user to input
* a pattern).
* @param displayMode How to display the pattern.
@@ -847,6 +864,16 @@
return null;
}
+ @Override
+ public boolean performHapticFeedback(int feedbackConstant, int flags) {
+ if (mExternalHapticsPlayer != null) {
+ mExternalHapticsPlayer.performCellAddedFeedback();
+ return true;
+ } else {
+ return super.performHapticFeedback(feedbackConstant, flags);
+ }
+ }
+
private void addCellToPattern(Cell newCell) {
mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
mPattern.add(newCell);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
index 4ce2942..bfccb29 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
@@ -16,16 +16,26 @@
package androidx.window.extensions.embedding;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
+
import android.os.Build;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue;
+import android.util.ArrayMap;
import android.util.Log;
+import android.util.SparseArray;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* Helper class to back up and restore the TaskFragmentOrganizer state, in order to resume
@@ -40,11 +50,21 @@
@NonNull
private final SplitController mController;
@NonNull
+ private final SplitPresenter mPresenter;
+ @NonNull
private final BackupIdler mBackupIdler = new BackupIdler();
private boolean mBackupIdlerScheduled;
- BackupHelper(@NonNull SplitController splitController, @NonNull Bundle savedState) {
+ private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList =
+ new ArrayList<>();
+ private final ArrayMap<IBinder, TaskFragmentInfo> mTaskFragmentInfos = new ArrayMap<>();
+ private final SparseArray<TaskFragmentParentInfo> mTaskFragmentParentInfos =
+ new SparseArray<>();
+
+ BackupHelper(@NonNull SplitController splitController, @NonNull SplitPresenter splitPresenter,
+ @NonNull Bundle savedState) {
mController = splitController;
+ mPresenter = splitPresenter;
if (!savedState.isEmpty()) {
restoreState(savedState);
@@ -67,13 +87,13 @@
public boolean queueIdle() {
synchronized (mController.mLock) {
mBackupIdlerScheduled = false;
- startBackup();
+ saveState();
}
return false;
}
}
- private void startBackup() {
+ private void saveState() {
final List<TaskContainer> taskContainers = mController.getTaskContainers();
if (taskContainers.isEmpty()) {
Log.w(TAG, "No task-container to back up");
@@ -97,13 +117,92 @@
return;
}
- final List<ParcelableTaskContainerData> parcelableTaskContainerDataList =
- savedState.getParcelableArrayList(KEY_TASK_CONTAINERS,
- ParcelableTaskContainerData.class);
- for (ParcelableTaskContainerData data : parcelableTaskContainerDataList) {
- final TaskContainer taskContainer = new TaskContainer(data, mController);
- if (DEBUG) Log.d(TAG, "Restoring task " + taskContainer.getTaskId());
- // TODO(b/289875940): implement the TaskContainer restoration.
+ if (DEBUG) Log.d(TAG, "Start restoring saved-state");
+ mParcelableTaskContainerDataList.addAll(savedState.getParcelableArrayList(
+ KEY_TASK_CONTAINERS, ParcelableTaskContainerData.class));
+ if (DEBUG) Log.d(TAG, "Retrieved tasks : " + mParcelableTaskContainerDataList.size());
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ return;
+ }
+
+ final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList(
+ KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class);
+ for (TaskFragmentInfo info : infos) {
+ if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+ mTaskFragmentInfos.put(info.getFragmentToken(), info);
+ mPresenter.updateTaskFragmentInfo(info);
+ }
+
+ final List<TaskFragmentParentInfo> parentInfos = savedState.getParcelableArrayList(
+ KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+ TaskFragmentParentInfo.class);
+ for (TaskFragmentParentInfo info : parentInfos) {
+ if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+ mTaskFragmentParentInfos.put(info.getTaskId(), info);
}
}
-}
+
+ boolean hasPendingStateToRestore() {
+ return !mParcelableTaskContainerDataList.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if any of the {@link TaskContainer} is restored.
+ * Otherwise, returns {@code false}.
+ */
+ boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+ @NonNull Set<EmbeddingRule> rules) {
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ return false;
+ }
+
+ if (DEBUG) Log.d(TAG, "Rebuilding TaskContainers.");
+ final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>();
+ for (EmbeddingRule rule : rules) {
+ embeddingRuleMap.put(rule.getTag(), rule);
+ }
+
+ boolean restoredAny = false;
+ for (int i = mParcelableTaskContainerDataList.size() - 1; i >= 0; i--) {
+ final ParcelableTaskContainerData parcelableTaskContainerData =
+ mParcelableTaskContainerDataList.get(i);
+ final List<String> tags = parcelableTaskContainerData.getSplitRuleTags();
+ if (!embeddingRuleMap.containsAll(tags)) {
+ // has unknown tag, unable to restore.
+ if (DEBUG) {
+ Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#"
+ + parcelableTaskContainerData.mTaskId);
+ }
+ continue;
+ }
+
+ mParcelableTaskContainerDataList.remove(parcelableTaskContainerData);
+ final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData,
+ mController, mTaskFragmentInfos);
+ if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer);
+ mController.addTaskContainer(taskContainer.getTaskId(), taskContainer);
+
+ for (ParcelableSplitContainerData splitData :
+ parcelableTaskContainerData.getParcelableSplitContainerDataList()) {
+ final SplitRule rule = (SplitRule) embeddingRuleMap.get(splitData.mSplitRuleTag);
+ assert rule != null;
+ if (mController.getContainer(splitData.getPrimaryContainerToken()) != null
+ && mController.getContainer(splitData.getSecondaryContainerToken())
+ != null) {
+ taskContainer.addSplitContainer(
+ new SplitContainer(splitData, mController, rule));
+ }
+ }
+
+ mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(),
+ mTaskFragmentParentInfos.get(taskContainer.getTaskId()));
+ restoredAny = true;
+ }
+
+ if (mParcelableTaskContainerDataList.isEmpty()) {
+ mTaskFragmentParentInfos.clear();
+ mTaskFragmentInfos.clear();
+ }
+ return restoredAny;
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
index 817cfce..cb280c5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
@@ -89,13 +89,13 @@
};
@NonNull
- private IBinder getPrimaryContainerToken() {
+ IBinder getPrimaryContainerToken() {
return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken()
: mPrimaryContainerToken;
}
@NonNull
- private IBinder getSecondaryContainerToken() {
+ IBinder getSecondaryContainerToken() {
return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken()
: mSecondaryContainerToken;
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
index 7377d00..97aa699 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
@@ -108,6 +108,15 @@
: mParcelableSplitContainerDataList;
}
+ @NonNull
+ List<String> getSplitRuleTags() {
+ final List<String> tags = new ArrayList<>();
+ for (ParcelableSplitContainerData data : getParcelableSplitContainerDataList()) {
+ tags.add(data.mSplitRuleTag);
+ }
+ return tags;
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 6d436ec..faf73c2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -86,6 +86,25 @@
}
}
+ /** This is only used when restoring it from a {@link ParcelableSplitContainerData}. */
+ SplitContainer(@NonNull ParcelableSplitContainerData parcelableData,
+ @NonNull SplitController splitController, @NonNull SplitRule splitRule) {
+ mParcelableData = parcelableData;
+ mPrimaryContainer = splitController.getContainer(parcelableData.getPrimaryContainerToken());
+ mSecondaryContainer = splitController.getContainer(
+ parcelableData.getSecondaryContainerToken());
+ mSplitRule = splitRule;
+ mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+ mCurrentSplitAttributes = mDefaultSplitAttributes;
+
+ if (shouldFinishPrimaryWithSecondary(splitRule)) {
+ mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer);
+ }
+ if (shouldFinishSecondaryWithPrimary(splitRule)) {
+ mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
+ }
+ }
+
void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) {
if (!mParcelableData.mIsPrimaryContainerMutable) {
throw new IllegalStateException("Cannot update primary TaskFragmentContainer");
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index f2f2b7ea..db4bb0e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -279,6 +279,26 @@
Log.i(TAG, "Setting embedding rules. Size: " + rules.size());
mSplitRules.clear();
mSplitRules.addAll(rules);
+
+ if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) {
+ return;
+ }
+
+ try {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ if (mPresenter.rebuildTaskContainers(wct, rules)) {
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ updateCallbackIfNecessary();
+ } else {
+ transactionRecord.abort();
+ }
+ } catch (IllegalStateException ex) {
+ Log.e(TAG, "Having an existing transaction while running restoration with"
+ + "new rules!! It is likely too late to perform the restoration "
+ + "already!?", ex);
+ }
}
}
@@ -903,6 +923,12 @@
}
@GuardedBy("mLock")
+ void onTaskFragmentParentRestored(@NonNull WindowContainerTransaction wct, int taskId,
+ @NonNull TaskFragmentParentInfo parentInfo) {
+ onTaskFragmentParentInfoChanged(wct, taskId, parentInfo);
+ }
+
+ @GuardedBy("mLock")
void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index abc7b29..0c0ded9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -24,6 +24,7 @@
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import android.annotation.AnimRes;
+import android.annotation.NonNull;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.WindowConfiguration;
@@ -47,7 +48,6 @@
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.embedding.SplitAttributes.SplitType;
@@ -67,6 +67,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -174,7 +175,7 @@
} else {
registerOrganizer();
}
- mBackupHelper = new BackupHelper(controller, outSavedState);
+ mBackupHelper = new BackupHelper(controller, this, outSavedState);
if (!SplitController.ENABLE_SHELL_TRANSITIONS) {
// TODO(b/207070762): cleanup with legacy app transition
// Animation will be handled by WM Shell when Shell transition is enabled.
@@ -186,6 +187,15 @@
mBackupHelper.scheduleBackup();
}
+ boolean isRebuildTaskContainersNeeded() {
+ return mBackupHelper.hasPendingStateToRestore();
+ }
+
+ boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+ @NonNull Set<EmbeddingRule> rules) {
+ return mBackupHelper.rebuildTaskContainers(wct, rules);
+ }
+
/**
* Deletes the specified container and all other associated and dependent containers in the same
* transaction.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 608a3be..74cce68 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -31,6 +31,7 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -147,14 +148,23 @@
/** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */
TaskContainer(@NonNull ParcelableTaskContainerData data,
- @NonNull SplitController splitController) {
+ @NonNull SplitController splitController,
+ @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) {
mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this);
+ mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */,
+ false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
mSplitController = splitController;
for (ParcelableTaskFragmentContainerData tfData :
data.getParcelableTaskFragmentContainerDataList()) {
- final TaskFragmentContainer container =
- new TaskFragmentContainer(tfData, splitController, this);
- mContainers.add(container);
+ final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken);
+ if (info != null && !info.isEmpty()) {
+ final TaskFragmentContainer container =
+ new TaskFragmentContainer(tfData, splitController, this);
+ container.setInfo(new WindowContainerTransaction(), info);
+ mContainers.add(container);
+ } else {
+ Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId);
+ }
}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index cf39415..6c83d88 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -29,7 +29,6 @@
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.TaskSnapshot;
/**
@@ -75,7 +74,7 @@
public PipColorOverlay(Context context) {
mContext = context;
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.setColorLayer()
@@ -123,7 +122,7 @@
public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.build();
@@ -183,7 +182,7 @@
mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
prepareAppIconOverlay(appIcon);
- mLeash = new SurfaceControl.Builder(new SurfaceSession())
+ mLeash = new SurfaceControl.Builder()
.setCallsite(TAG)
.setName(LAYER_NAME)
.build();
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 7b3b207..1563994 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
@@ -982,7 +982,6 @@
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
mReceivedNullNavigationInfo = false;
- mBackTransitionHandler.mLastTrigger = triggerBack;
if (mBackNavigationInfo != null) {
mPreviousNavigationType = mBackNavigationInfo.getType();
mBackNavigationInfo.onBackNavigationFinished(triggerBack);
@@ -1103,7 +1102,6 @@
endLatencyTracking();
if (!validateAnimationTargets(apps)) {
Log.e(TAG, "Invalid animation targets!");
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
return;
}
mBackAnimationFinishedCallback = finishedCallback;
@@ -1113,7 +1111,6 @@
return;
}
kickStartAnimation();
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
});
}
@@ -1121,7 +1118,6 @@
public void onAnimationCancelled() {
mShellExecutor.execute(
() -> {
- mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
if (!mShellBackAnimationRegistry.cancel(
mBackNavigationInfo != null
? mBackNavigationInfo.getType()
@@ -1160,8 +1156,6 @@
boolean mCloseTransitionRequested;
SurfaceControl.Transaction mFinishOpenTransaction;
Transitions.TransitionFinishCallback mFinishOpenTransitionCallback;
- QueuedTransition mQueuedTransition = null;
- boolean mLastTrigger;
// The Transition to make behindActivity become visible
IBinder mPrepareOpenTransition;
// The Transition to make behindActivity become invisible, if prepare open exist and
@@ -1178,13 +1172,6 @@
}
}
- void consumeQueuedTransitionIfNeeded() {
- if (mQueuedTransition != null) {
- mQueuedTransition.consume();
- mQueuedTransition = null;
- }
- }
-
private void applyFinishOpenTransition() {
mOpenTransitionInfo = null;
mPrepareOpenTransition = null;
@@ -1215,7 +1202,9 @@
@NonNull SurfaceControl.Transaction st,
@NonNull SurfaceControl.Transaction ft,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
+ final boolean isPrepareTransition =
+ info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
+ if (isPrepareTransition) {
kickStartAnimation();
}
// Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't
@@ -1240,21 +1229,14 @@
}
if (mApps == null || mApps.length == 0) {
- if (mBackNavigationInfo != null && mShellBackAnimationRegistry
- .isWaitingAnimation(mBackNavigationInfo.getType())) {
- // Waiting for animation? Queue update to wait for animation start.
- consumeQueuedTransitionIfNeeded();
- mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback);
- return true;
- } else if (mLastTrigger) {
- // animation was done, consume directly
+ if (mCloseTransitionRequested) {
+ // animation never start, consume directly
applyAndFinish(st, ft, finishCallback);
return true;
- } else {
- // animation was cancelled but transition haven't happen, we must handle it
- if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) {
- createClosePrepareTransition();
- }
+ } else if (mClosePrepareTransition == null && isPrepareTransition) {
+ // Gesture animation was cancelled before prepare transition ready, create the
+ // the close prepare transition
+ createClosePrepareTransition();
}
}
@@ -1413,9 +1395,6 @@
if (mPrepareOpenTransition != null) {
applyFinishOpenTransition();
}
- if (mQueuedTransition != null) {
- consumeQueuedTransitionIfNeeded();
- }
return;
}
// Handle the commit transition if this handler is running the open transition.
@@ -1423,11 +1402,9 @@
t.apply();
if (mCloseTransitionRequested) {
if (mApps == null || mApps.length == 0) {
- if (mQueuedTransition == null) {
- // animation was done
- applyFinishOpenTransition();
- mCloseTransitionRequested = false;
- } // let queued transition finish.
+ // animation was done
+ applyFinishOpenTransition();
+ mCloseTransitionRequested = false;
} else {
// we are animating, wait until animation finish
mOnAnimationFinishCallback = () -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
index 4b138e4..dd17e29 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
@@ -17,7 +17,6 @@
package com.android.wm.shell.common;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
/**
* Helpers for handling surface.
@@ -25,16 +24,15 @@
public class SurfaceUtils {
/** Creates a dim layer above host surface. */
public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host,
- String name, SurfaceSession surfaceSession) {
- final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession);
+ String name) {
+ final SurfaceControl dimLayer = makeColorLayer(host, name);
t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f});
return dimLayer;
}
/** Creates a color layer for host surface. */
- public static SurfaceControl makeColorLayer(SurfaceControl host, String name,
- SurfaceSession surfaceSession) {
- return new SurfaceControl.Builder(surfaceSession)
+ public static SurfaceControl makeColorLayer(SurfaceControl host, String name) {
+ return new SurfaceControl.Builder()
.setParent(host)
.setColorLayer()
.setName(name)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index ef33b38..3dc86de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -42,7 +42,6 @@
import android.view.ScrollCaptureResponse;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -311,7 +310,7 @@
@Override
protected SurfaceControl getParentSurface(IWindow window,
WindowManager.LayoutParams attrs) {
- SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession())
+ SurfaceControl leash = new SurfaceControl.Builder()
.setContainerLayer()
.setName("SystemWindowLeash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 7175e36..de3152a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -43,7 +43,6 @@
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -74,7 +73,6 @@
private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
private final IconProvider mIconProvider;
- private final SurfaceSession mSurfaceSession;
private Drawable mIcon;
private ImageView mVeilIconView;
@@ -103,17 +101,15 @@
private int mOffsetY;
private int mRunningAnimationCount = 0;
- public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
- SurfaceSession surfaceSession) {
+ public SplitDecorManager(Configuration configuration, IconProvider iconProvider) {
super(configuration, null /* rootSurface */, null /* hostInputToken */);
mIconProvider = iconProvider;
- mSurfaceSession = surfaceSession;
}
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(TAG)
.setHidden(true)
@@ -238,7 +234,7 @@
if (mBackgroundLeash == null) {
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ RESIZING_BACKGROUND_SURFACE_NAME);
t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
@@ -248,7 +244,7 @@
final int left = isLandscape ? mOldMainBounds.width() : 0;
final int top = isLandscape ? 0 : mOldMainBounds.height();
mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ GAP_BACKGROUND_SURFACE_NAME);
// Fill up another side bounds area.
t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
@@ -405,7 +401,7 @@
if (mBackgroundLeash == null) {
// Initialize background
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
- RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+ RESIZING_BACKGROUND_SURFACE_NAME);
t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 46c1a43..c5f1974 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -36,7 +36,6 @@
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -98,7 +97,7 @@
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(TAG)
.setHidden(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 0564c95..d2b4f1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -38,7 +38,6 @@
import android.view.IWindow;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -173,7 +172,7 @@
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
String className = getClass().getSimpleName();
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName(className + "Leash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
index 831b331..abc26cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
@@ -24,7 +24,6 @@
import android.view.IWindow
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
import android.view.View
import android.view.WindowManager
import android.view.WindowlessWindowManager
@@ -106,7 +105,7 @@
attrs: WindowManager.LayoutParams
): SurfaceControl? {
val className = javaClass.simpleName
- val builder = SurfaceControl.Builder(SurfaceSession())
+ val builder = SurfaceControl.Builder()
.setContainerLayer()
.setName(className + "Leash")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
index 71cc8df..422656c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
@@ -38,7 +38,6 @@
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -105,7 +104,7 @@
@Override
protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setColorLayer()
.setBufferSize(mDisplayBounds.width(), mDisplayBounds.height())
.setFormat(PixelFormat.RGB_888)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 7e165af..793e2aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -61,7 +61,6 @@
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.RemoteTransition;
@@ -897,7 +896,7 @@
private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps,
SurfaceControl.Transaction t, String callsite) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName("RecentsAnimationSplitTasks")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index f3113dc..dad0d4e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -104,7 +104,6 @@
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.DisplayAreaInfo;
@@ -171,8 +170,6 @@
private static final String TAG = StageCoordinator.class.getSimpleName();
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
private final StageTaskListener mMainStage;
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
private final StageTaskListener mSideStage;
@@ -334,7 +331,6 @@
mDisplayId,
mMainStageListener,
mSyncQueue,
- mSurfaceSession,
iconProvider,
mWindowDecorViewModel);
mSideStage = new StageTaskListener(
@@ -343,7 +339,6 @@
mDisplayId,
mSideStageListener,
mSyncQueue,
- mSurfaceSession,
iconProvider,
mWindowDecorViewModel);
mDisplayController = displayController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 29b5114..d64c0a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -39,7 +39,6 @@
import android.util.SparseArray;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -93,7 +92,6 @@
private final Context mContext;
private final StageListenerCallbacks mCallbacks;
- private final SurfaceSession mSurfaceSession;
private final SyncTransactionQueue mSyncQueue;
private final IconProvider mIconProvider;
private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
@@ -108,12 +106,11 @@
StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
- SurfaceSession surfaceSession, IconProvider iconProvider,
+ IconProvider iconProvider,
Optional<WindowDecorViewModel> windowDecorViewModel) {
mContext = context;
mCallbacks = callbacks;
mSyncQueue = syncQueue;
- mSurfaceSession = surfaceSession;
mIconProvider = iconProvider;
mWindowDecorViewModel = windowDecorViewModel;
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
@@ -203,12 +200,11 @@
mRootTaskInfo = taskInfo;
mSplitDecorManager = new SplitDecorManager(
mRootTaskInfo.configuration,
- mIconProvider,
- mSurfaceSession);
+ mIconProvider);
mCallbacks.onRootTaskAppeared();
sendStatusChanged();
mSyncQueue.runInSync(t -> mDimLayer =
- SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession));
+ SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer"));
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index fac3592..2e9b53e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -33,7 +33,6 @@
import android.util.SparseArray;
import android.view.IWindow;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.SplashScreenView;
@@ -204,7 +203,7 @@
@Override
protected SurfaceControl getParentSurface(IWindow window,
WindowManager.LayoutParams attrs) {
- final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+ final SurfaceControl.Builder builder = new SurfaceControl.Builder()
.setContainerLayer()
.setName("Windowless window")
.setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 4fc6c44..ff4b981 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -92,7 +92,6 @@
import android.util.ArrayMap;
import android.view.Choreographer;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
@@ -134,8 +133,6 @@
private final TransitionAnimation mTransitionAnimation;
private final DevicePolicyManager mDevicePolicyManager;
- private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
/** Keeps track of the currently-running animations associated with each transition. */
private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
@@ -705,7 +702,7 @@
TransitionInfo.Change change, TransitionInfo info, int animHint,
ArrayList<Animator> animations, Runnable onAnimFinish) {
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
- final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
+ final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext,
mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
animHint);
// The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
@@ -918,7 +915,7 @@
}
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(
change.getLeash(), thumbnail, transaction);
final Animation a =
mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds);
@@ -943,7 +940,7 @@
@NonNull Runnable finishCallback, TransitionInfo.Change change,
TransitionInfo.AnimationOptions options, float cornerRadius) {
final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
- final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+ final WindowThumbnail wt = WindowThumbnail.createAndAttach(
change.getLeash(), options.getThumbnail(), transaction);
final Rect bounds = change.getEndAbsBounds();
final int orientation = mContext.getResources().getConfiguration().orientation;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 3d79a1c..c385f9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -74,8 +74,12 @@
final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
if (Flags.migratePredictiveBackTransition()) {
- if (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode)) {
- notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode));
+ final boolean gestureToHomeTransition = isBackGesture
+ && TransitionUtil.isClosingType(info.getType());
+ if (gestureToHomeTransition
+ || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) {
+ notifyHomeVisibilityChanged(gestureToHomeTransition
+ || TransitionUtil.isOpeningType(mode));
}
} else {
if (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 0bf9d36..5802e2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -38,7 +38,6 @@
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
-import android.view.SurfaceSession;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.window.ScreenCapture;
@@ -112,7 +111,7 @@
/** Intensity of light/whiteness of the layout after rotation occurs. */
private float mEndLuma;
- ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool,
+ ScreenRotationAnimation(Context context, TransactionPool pool,
Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) {
mContext = context;
mTransactionPool = pool;
@@ -126,7 +125,7 @@
mStartRotation = change.getStartRotation();
mEndRotation = change.getEndRotation();
- mAnimLeash = new SurfaceControl.Builder(session)
+ mAnimLeash = new SurfaceControl.Builder()
.setParent(rootLeash)
.setEffectLayer()
.setCallsite("ShellRotationAnimation")
@@ -153,7 +152,7 @@
return;
}
- mScreenshotLayer = new SurfaceControl.Builder(session)
+ mScreenshotLayer = new SurfaceControl.Builder()
.setParent(mAnimLeash)
.setBLASTLayer()
.setSecure(screenshotBuffer.containsSecureLayers())
@@ -178,7 +177,7 @@
t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
if (!isCustomRotate()) {
- mBackColorSurface = new SurfaceControl.Builder(session)
+ mBackColorSurface = new SurfaceControl.Builder()
.setParent(rootLeash)
.setColorLayer()
.setOpaque(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
index 2c668ed..341f2bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
@@ -21,7 +21,6 @@
import android.graphics.PixelFormat;
import android.hardware.HardwareBuffer;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
/**
* Represents a surface that is displayed over a transition surface.
@@ -33,10 +32,10 @@
private WindowThumbnail() {}
/** Create a thumbnail surface and attach it over a parent surface. */
- static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent,
+ static WindowThumbnail createAndAttach(SurfaceControl parent,
HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) {
WindowThumbnail windowThumbnail = new WindowThumbnail();
- windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession)
+ windowThumbnail.mSurfaceControl = new SurfaceControl.Builder()
.setParent(parent)
.setName("WindowThumanil : " + parent.toString())
.setCallsite("WindowThumanil")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fd6c4d8..fb81ed4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -30,7 +30,6 @@
import android.view.LayoutInflater
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL
import android.view.WindowlessWindowManager
@@ -66,7 +65,6 @@
private val lightColors = dynamicLightColorScheme(context)
private val darkColors = dynamicDarkColorScheme(context)
- private val surfaceSession = SurfaceSession()
private lateinit var iconView: ImageView
private var iconSize = 0
@@ -126,7 +124,7 @@
.setCallsite("ResizeVeil#setupResizeVeil")
.build()
backgroundSurface = surfaceControlBuilderFactory
- .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession)
+ .create("Resize veil background of Task=" + taskInfo.taskId)
.setColorLayer()
.setHidden(true)
.setParent(veilSurface)
@@ -399,10 +397,6 @@
fun create(name: String): SurfaceControl.Builder {
return SurfaceControl.Builder().setName(name)
}
-
- fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder {
- return SurfaceControl.Builder(surfaceSession).setName(name)
- }
}
companion object {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index dfa5ab4..9ef4b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -29,6 +29,7 @@
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.view.WindowManager
import android.widget.ImageButton
+import com.android.internal.policy.SystemBarUtils
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.shared.animation.Interpolators
@@ -74,7 +75,10 @@
) {
captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
this.taskInfo = taskInfo
- if (!isCaptionVisible && hasStatusBarInputLayer()) {
+ // If handle is not in status bar region(i.e., bottom stage in vertical split),
+ // do not create an input layer
+ if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return
+ if (!isCaptionVisible && hasStatusBarInputLayer() ) {
disposeStatusBarInputLayer()
return
}
@@ -120,7 +124,7 @@
inputManager.pilferPointers(v.viewRootImpl.inputToken)
}
captionHandle.dispatchTouchEvent(event)
- true
+ return@setOnTouchListener true
}
windowManagerWrapper.updateViewLayout(view, lp)
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index 426f40b..a54d497 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.scenarios
import android.app.Instrumentation
-import android.platform.test.annotations.Postsubmit
import android.tools.NavBar
import android.tools.Rotation
import android.tools.flicker.rules.ChangeDisplayOrientationRule
@@ -33,15 +32,12 @@
import org.junit.After
import org.junit.Assume
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.BlockJUnit4ClassRunner
-@RunWith(BlockJUnit4ClassRunner::class)
-@Postsubmit
-open class MaximizeAppWindow
-@JvmOverloads
+@Ignore("Test Base Class")
+abstract class MaximizeAppWindow
constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 413e495..e514dc3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -49,7 +49,6 @@
import android.os.RemoteException;
import android.util.SparseArray;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.ITaskOrganizer;
import android.window.ITaskOrganizerController;
import android.window.TaskAppearedInfo;
@@ -169,7 +168,7 @@
public void testTaskLeashReleasedAfterVanished() throws RemoteException {
assumeFalse(ENABLE_SHELL_TRANSITIONS);
RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
- SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
+ SurfaceControl taskLeash = new SurfaceControl.Builder()
.setName("task").build();
mOrganizer.registerOrganizer();
mOrganizer.onTaskAppeared(taskInfo, taskLeash);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 751275b..66dcef6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -24,7 +24,6 @@
import android.graphics.Rect;
import android.os.Handler;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -89,7 +88,7 @@
// Prepare root task for testing.
mRootTask = new TestRunningTaskInfoBuilder().build();
- mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build();
+ mRootLeash = new SurfaceControl.Builder().setName("test").build();
onTaskAppeared(mRootTask, mRootLeash);
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index af288c8..ce3944a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -53,7 +53,6 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.IRemoteTransition;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
@@ -106,7 +105,6 @@
@Mock private DisplayInsetsController mDisplayInsetsController;
@Mock private TransactionPool mTransactionPool;
@Mock private Transitions mTransitions;
- @Mock private SurfaceSession mSurfaceSession;
@Mock private IconProvider mIconProvider;
@Mock private WindowDecorViewModel mWindowDecorViewModel;
@Mock private ShellExecutor mMainExecutor;
@@ -134,11 +132,11 @@
doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
mSplitLayout = SplitTestUtils.createMockSplitLayout();
mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
- StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
mIconProvider, Optional.of(mWindowDecorViewModel)));
mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
- StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
mIconProvider, Optional.of(mWindowDecorViewModel)));
mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index ce343b8..a6c16c4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -50,7 +50,6 @@
import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.RemoteTransition;
import android.window.WindowContainerTransaction;
@@ -119,7 +118,6 @@
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
private final Rect mRootBounds = new Rect(0, 0, 45, 60);
- private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mRootLeash;
private SurfaceControl mDividerLeash;
private ActivityManager.RunningTaskInfo mRootTask;
@@ -139,7 +137,7 @@
mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController,
Optional.empty()));
- mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
+ mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build();
when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
@@ -149,7 +147,7 @@
when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash);
mRootTask = new TestRunningTaskInfoBuilder().build();
- mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+ mRootLeash = new SurfaceControl.Builder().setName("test").build();
mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash);
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 8b5cb97..b7b7d0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -32,7 +32,6 @@
import android.app.ActivityManager;
import android.os.SystemProperties;
import android.view.SurfaceControl;
-import android.view.SurfaceSession;
import android.window.WindowContainerTransaction;
import androidx.test.annotation.UiThreadTest;
@@ -82,7 +81,6 @@
private WindowContainerTransaction mWct;
@Captor
private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
- private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mSurfaceControl;
private ActivityManager.RunningTaskInfo mRootTask;
private StageTaskListener mStageTaskListener;
@@ -97,12 +95,11 @@
DEFAULT_DISPLAY,
mCallbacks,
mSyncQueue,
- mSurfaceSession,
mIconProvider,
Optional.of(mWindowDecorViewModel));
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootTask.parentTaskId = INVALID_TASK_ID;
- mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+ mSurfaceControl = new SurfaceControl.Builder().setName("test").build();
mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index 1984885..17fd95b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -49,7 +49,6 @@
import android.testing.TestableLooper;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
-import android.view.SurfaceSession;
import android.view.ViewTreeObserver;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -95,7 +94,6 @@
Looper mViewLooper;
TestHandler mViewHandler;
- SurfaceSession mSession;
SurfaceControl mLeash;
Context mContext;
@@ -106,7 +104,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mLeash = new SurfaceControl.Builder(mSession)
+ mLeash = new SurfaceControl.Builder()
.setName("test")
.build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index f51a960..8f49de0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -20,6 +20,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
@@ -39,6 +40,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.view.SurfaceControl;
@@ -213,6 +215,35 @@
verify(mListener, times(1)).onHomeVisibilityChanged(true);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION)
+ public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose()
+ throws RemoteException {
+ TransitionInfo info = mock(TransitionInfo.class);
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+ when(change.getTaskInfo()).thenReturn(taskInfo);
+ when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+ when(info.getType()).thenReturn(TRANSIT_PREPARE_BACK_NAVIGATION);
+
+ when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true);
+ setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true);
+
+ mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+ info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class));
+ verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
+
+ when(info.getType()).thenReturn(TRANSIT_TO_BACK);
+ setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true);
+ mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+ info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class));
+ verify(mListener, times(1)).onHomeVisibilityChanged(true);
+ }
+
/**
* Helper class to initialize variables for the rest.
*/
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
index a07be79..e0d16aa 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -97,7 +97,7 @@
.thenReturn(spyResizeVeilSurfaceBuilder)
doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build()
whenever(mockSurfaceControlBuilderFactory
- .create(eq("Resize veil background of Task=" + taskInfo.taskId), any()))
+ .create(eq("Resize veil background of Task=" + taskInfo.taskId)))
.thenReturn(spyBackgroundSurfaceBuilder)
doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build()
whenever(mockSurfaceControlBuilderFactory
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index ff09084..c4173ed 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -460,7 +460,7 @@
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
event.startTracking();
return true;
}
@@ -479,7 +479,7 @@
return true;
}
- if (keyCode == KeyEvent.KEYCODE_BACK
+ if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
&& event.isTracking() && !event.isCanceled()) {
if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
&& !hasErrors()) {
diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp
new file mode 100644
index 0000000..207637f
--- /dev/null
+++ b/packages/SettingsLib/Metadata/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibMetadata-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibMetadata",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibMetadata-srcs"],
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.fragment_fragment",
+ "guava",
+ "SettingsLibDataStore",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Metadata/AndroidManifest.xml b/packages/SettingsLib/Metadata/AndroidManifest.xml
new file mode 100644
index 0000000..1c801e6
--- /dev/null
+++ b/packages/SettingsLib/Metadata/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.metadata">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Metadata/processor/Android.bp b/packages/SettingsLib/Metadata/processor/Android.bp
new file mode 100644
index 0000000..d8acc76
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/Android.bp
@@ -0,0 +1,11 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_plugin {
+ name: "SettingsLibMetadata-processor",
+ srcs: ["src/**/*.kt"],
+ processor_class: "com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor",
+ java_resource_dirs: ["resources"],
+ visibility: ["//visibility:public"],
+}
diff --git a/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..762a01a
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor
\ No newline at end of file
diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
new file mode 100644
index 0000000..620d717
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.metadata
+
+import java.util.TreeMap
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic
+
+/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
+class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
+ private val screens = TreeMap<String, ConstructorType>()
+ private val overlays = mutableMapOf<String, String>()
+ private val contextType: TypeMirror by lazy {
+ processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
+ }
+
+ private var options: Map<String, Any?>? = null
+ private lateinit var annotationElement: TypeElement
+ private lateinit var optionsElement: TypeElement
+ private lateinit var screenType: TypeMirror
+
+ override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
+
+ override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+ override fun init(processingEnv: ProcessingEnvironment) {
+ super.init(processingEnv)
+ val elementUtils = processingEnv.elementUtils
+ annotationElement = elementUtils.getTypeElement(ANNOTATION)
+ optionsElement = elementUtils.getTypeElement(OPTIONS)
+ screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
+ }
+
+ override fun process(
+ annotations: MutableSet<out TypeElement>,
+ roundEnv: RoundEnvironment,
+ ): Boolean {
+ roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
+ if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
+ options =
+ annotationMirrors
+ .single { it.isElement(optionsElement) }
+ .elementValues
+ .entries
+ .associate { it.key.simpleName.toString() to it.value.value }
+ }
+ for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
+ (element as? TypeElement)?.process()
+ }
+ if (roundEnv.processingOver()) codegen()
+ return false
+ }
+
+ private fun TypeElement.process() {
+ if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
+ error("@$ANNOTATION_NAME must be added to non abstract class", this)
+ return
+ }
+ if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
+ error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
+ return
+ }
+ val constructorType = getConstructorType()
+ if (constructorType == null) {
+ error(
+ "Class must be an object, or has single public constructor that " +
+ "accepts no parameter or a Context parameter",
+ this,
+ )
+ return
+ }
+ val screenQualifiedName = qualifiedName.toString()
+ screens[screenQualifiedName] = constructorType
+ val annotation = annotationMirrors.single { it.isElement(annotationElement) }
+ val overlay = annotation.getOverlay()
+ if (overlay != null) {
+ overlays.put(overlay, screenQualifiedName)?.let {
+ error("$overlay has been overlaid by $it", this)
+ }
+ }
+ }
+
+ private fun codegen() {
+ val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
+ if (collector.isEmpty()) return
+ val parts = collector.split('/')
+ if (parts.size == 3) {
+ generateCode(parts[0], parts[1], parts[2])
+ } else {
+ throw IllegalArgumentException(
+ "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
+ )
+ }
+ }
+
+ private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
+ for ((overlay, screen) in overlays) {
+ if (screens.remove(overlay) == null) {
+ warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME")
+ } else {
+ processingEnv.messager.printMessage(
+ Diagnostic.Kind.NOTE,
+ "$overlay is overlaid by $screen",
+ )
+ }
+ }
+ processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
+ it.write("package $outputPkg;\n\n")
+ it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n")
+ it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n")
+ it.write("public final class $outputClass {\n")
+ it.write(" private $outputClass() {}\n\n")
+ it.write(
+ " public static java.util.List<$PREFERENCE_SCREEN_METADATA> " +
+ "$outputFun(android.content.Context context) {\n"
+ )
+ it.write(
+ " java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " +
+ "new java.util.ArrayList<>(${screens.size});\n"
+ )
+ for ((screen, constructorType) in screens) {
+ when (constructorType) {
+ ConstructorType.DEFAULT -> it.write(" screens.add(new $screen());\n")
+ ConstructorType.CONTEXT -> it.write(" screens.add(new $screen(context));\n")
+ ConstructorType.SINGLETON -> it.write(" screens.add($screen.INSTANCE);\n")
+ }
+ }
+ for ((overlay, screen) in overlays) {
+ it.write(" // $overlay is overlaid by $screen\n")
+ }
+ it.write(" return screens;\n")
+ it.write(" }\n")
+ it.write("}")
+ }
+ }
+
+ private fun AnnotationMirror.isElement(element: TypeElement) =
+ processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
+
+ private fun AnnotationMirror.getOverlay(): String? {
+ for ((key, value) in elementValues) {
+ if (key.simpleName.contentEquals("overlay")) {
+ return if (value.isDefaultClassValue(key)) null else value.value.toString()
+ }
+ }
+ return null
+ }
+
+ private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) =
+ processingEnv.typeUtils.isSameType(
+ value as TypeMirror,
+ key.defaultValue.value as TypeMirror,
+ )
+
+ private fun TypeElement.getConstructorType(): ConstructorType? {
+ var constructor: ExecutableElement? = null
+ for (element in enclosedElements) {
+ if (element.isKotlinObject()) return ConstructorType.SINGLETON
+ if (element.kind != ElementKind.CONSTRUCTOR) continue
+ if (!element.modifiers.contains(Modifier.PUBLIC)) continue
+ if (constructor != null) return null
+ constructor = element as ExecutableElement
+ }
+ return constructor?.parameters?.run {
+ when {
+ isEmpty() -> ConstructorType.DEFAULT
+ size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) ->
+ ConstructorType.CONTEXT
+ else -> null
+ }
+ }
+ }
+
+ private fun Element.isKotlinObject() =
+ kind == ElementKind.FIELD &&
+ modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } &&
+ simpleName.toString() == "INSTANCE"
+
+ private fun warn(msg: CharSequence) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
+
+ private fun error(msg: CharSequence, element: Element) =
+ processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
+
+ private enum class ConstructorType {
+ DEFAULT, // default constructor with no parameter
+ CONTEXT, // constructor with a Context parameter
+ SINGLETON, // Kotlin object class
+ }
+
+ companion object {
+ private const val PACKAGE = "com.android.settingslib.metadata"
+ private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
+ private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
+ private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
+
+ private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
+ private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
+ private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
new file mode 100644
index 0000000..ea20a74
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.metadata
+
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to provide preference screen.
+ *
+ * The annotated class must satisfy either condition:
+ * - the primary constructor has no parameter
+ * - the primary constructor has a single [android.content.Context] parameter
+ * - it is a Kotlin object class
+ *
+ * @param overlay if specified, current annotated screen will overlay the given screen
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreen(
+ val overlay: KClass<out PreferenceScreenMetadata> = PreferenceScreenMetadata::class,
+)
+
+/**
+ * Provides options for [ProvidePreferenceScreen] annotation processor.
+ *
+ * @param codegenCollector generated collector class (format: "pkg/class/method"), an empty string
+ * means do not generate code
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreenOptions(
+ val codegenCollector: String = "com.android.settingslib.metadata/PreferenceScreenCollector/get",
+)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
new file mode 100644
index 0000000..51a8580
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+import androidx.annotation.ArrayRes
+import androidx.annotation.IntDef
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Permit of read and write request. */
+@IntDef(
+ ReadWritePermit.ALLOW,
+ ReadWritePermit.DISALLOW,
+ ReadWritePermit.REQUIRE_APP_PERMISSION,
+ ReadWritePermit.REQUIRE_USER_AGREEMENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ReadWritePermit {
+ companion object {
+ /** Allow to read/write value. */
+ const val ALLOW = 0
+ /** Disallow to read/write value (e.g. uid not allowed). */
+ const val DISALLOW = 1
+ /** Require (runtime/special) app permission from user explicitly. */
+ const val REQUIRE_APP_PERMISSION = 2
+ /** Require explicit user agreement (e.g. terms of service). */
+ const val REQUIRE_USER_AGREEMENT = 3
+ }
+}
+
+/** Preference interface that has a value persisted in datastore. */
+interface PersistentPreference<T> {
+
+ /**
+ * Returns the key-value storage of the preference.
+ *
+ * The default implementation returns the storage provided by
+ * [PreferenceScreenRegistry.getKeyValueStore].
+ */
+ fun storage(context: Context): KeyValueStore =
+ PreferenceScreenRegistry.getKeyValueStore(context, this as PreferenceMetadata)!!
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to read
+ * preference value.
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getReadPermit(context: Context, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getReadPermit(
+ context,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+
+ /**
+ * Returns if the external application (identified by [callingUid]) has permission to write
+ * preference with given [value].
+ *
+ * The underlying implementation does NOT need to check common states like isEnabled,
+ * isRestricted or isAvailable.
+ */
+ @ReadWritePermit
+ fun getWritePermit(context: Context, value: T?, myUid: Int, callingUid: Int): Int =
+ PreferenceScreenRegistry.getWritePermit(
+ context,
+ value,
+ myUid,
+ callingUid,
+ this as PreferenceMetadata,
+ )
+}
+
+/** Descriptor of values. */
+sealed interface ValueDescriptor {
+
+ /** Returns if given value (represented by index) is valid. */
+ fun isValidValue(context: Context, index: Int): Boolean
+}
+
+/**
+ * A boolean type value.
+ *
+ * A zero value means `False`, otherwise it is `True`.
+ */
+interface BooleanValue : ValueDescriptor {
+ override fun isValidValue(context: Context, index: Int) = true
+}
+
+/** Value falls into a given array. */
+interface DiscreteValue<T> : ValueDescriptor {
+ @get:ArrayRes val values: Int
+
+ @get:ArrayRes val valuesDescription: Int
+
+ fun getValue(context: Context, index: Int): T
+}
+
+/**
+ * Value falls into a text array, whose element is [CharSequence] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteTextValue : DiscreteValue<CharSequence> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getTextArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): CharSequence =
+ context.resources.getTextArray(values)[index]
+}
+
+/**
+ * Value falls into a string array, whose element is [String] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteStringValue : DiscreteValue<String> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getStringArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): String =
+ context.resources.getStringArray(values)[index]
+}
+
+/**
+ * Value falls into an integer array.
+ *
+ * [values] resource is `<integer-array>`.
+ */
+interface DiscreteIntValue : DiscreteValue<Int> {
+ override fun isValidValue(context: Context, index: Int): Boolean {
+ if (index < 0) return false
+ return index < context.resources.getIntArray(values).size
+ }
+
+ override fun getValue(context: Context, index: Int): Int =
+ context.resources.getIntArray(values)[index]
+}
+
+/** Value is between a range. */
+interface RangeValue : ValueDescriptor {
+ /** The lower bound (inclusive) of the range. */
+ val minValue: Int
+
+ /** The upper bound (inclusive) of the range. */
+ val maxValue: Int
+
+ /** The increment step within the range. 0 means unset, which implies step size is 1. */
+ val incrementStep: Int
+ get() = 0
+
+ override fun isValidValue(context: Context, index: Int) = index in minValue..maxValue
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
new file mode 100644
index 0000000..4503738
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.metadata
+
+/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
+open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata)
+
+/**
+ * Preference hierarchy describes the structure of preferences recursively.
+ *
+ * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group.
+ */
+class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) :
+ PreferenceHierarchyNode(metadata) {
+
+ private val children = mutableListOf<PreferenceHierarchyNode>()
+
+ /** Adds a preference to the hierarchy. */
+ operator fun PreferenceMetadata.unaryPlus() {
+ children.add(PreferenceHierarchyNode(this))
+ }
+
+ /**
+ * Adds preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ operator fun String.unaryPlus() {
+ children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!))
+ }
+
+ /** Adds a preference to the hierarchy. */
+ fun add(metadata: PreferenceMetadata) {
+ children.add(PreferenceHierarchyNode(metadata))
+ }
+
+ /** Adds a preference group to the hierarchy. */
+ operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) }
+
+ /** Adds a preference group and returns its preference hierarchy. */
+ fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy =
+ PreferenceHierarchy(metadata).also { children.add(it) }
+
+ /**
+ * Adds preference screen with given key (as a placeholder) to the hierarchy.
+ *
+ * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+ * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+ * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+ * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+ * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+ *
+ * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+ */
+ fun addPreferenceScreen(screenKey: String) {
+ children.add(PreferenceHierarchy(PreferenceScreenRegistry[screenKey]!!))
+ }
+
+ /** Extensions to add more preferences to the hierarchy. */
+ operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
+
+ /** Traversals preference hierarchy and applies given action. */
+ fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Traversals preference hierarchy and applies given action. */
+ suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
+ for (it in children) action(it)
+ }
+
+ /** Finds the [PreferenceMetadata] object associated with given key. */
+ fun find(key: String): PreferenceMetadata? {
+ if (metadata.key == key) return metadata
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ val result = child.find(key)
+ if (result != null) return result
+ } else {
+ if (child.metadata.key == key) return child.metadata
+ }
+ }
+ return null
+ }
+
+ /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */
+ fun getAllPreferences(): List<PreferenceMetadata> =
+ mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) }
+
+ private fun getAllPreferences(result: MutableList<PreferenceMetadata>) {
+ result.add(metadata)
+ for (child in children) {
+ if (child is PreferenceHierarchy) {
+ child.getAllPreferences(result)
+ } else {
+ result.add(child.metadata)
+ }
+ }
+ }
+}
+
+/**
+ * Builder function to create [PreferenceHierarchy] in
+ * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner.
+ */
+fun preferenceHierarchy(metadata: PreferenceMetadata, init: PreferenceHierarchy.() -> Unit) =
+ PreferenceHierarchy(metadata).also(init)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
new file mode 100644
index 0000000..f39f3a0
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.annotation.AnyThread
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/**
+ * Interface provides preference metadata (title, summary, icon, etc.).
+ *
+ * Besides the existing APIs, subclass could integrate with following interface to provide more
+ * information:
+ * - [PreferenceTitleProvider]: provide dynamic title content
+ * - [PreferenceSummaryProvider]: provide dynamic summary content (e.g. based on preference value)
+ * - [PreferenceAvailabilityProvider]: provide preference availability (e.g. based on flag)
+ * - [PreferenceLifecycleProvider]: provide the lifecycle callbacks and notify state change
+ *
+ * Notes:
+ * - UI framework support:
+ * - This class does not involve any UI logic, it is the data layer.
+ * - Subclass could integrate with datastore and UI widget to provide UI layer. For instance,
+ * `PreferenceBinding` supports Jetpack Preference binding.
+ * - Datastore:
+ * - Subclass should implement the [PersistentPreference] to note that current preference is
+ * persistent in datastore.
+ * - It is always recommended to support back up preference value changed by user. Typically,
+ * the back up and restore happen within datastore, the [allowBackup] API is to mark if
+ * current preference value should be backed up (backup allowed by default).
+ * - Preference indexing for search:
+ * - Override [isIndexable] API to mark if preference is indexable (enabled by default).
+ * - If [isIndexable] returns true, preference title and summary will be indexed with cache.
+ * More indexing data could be provided through [keywords].
+ * - Settings search will cache the preference title/summary/keywords for indexing. The cache is
+ * invalidated when system locale changed, app upgraded, etc.
+ * - Dynamic content is not suitable to be cached for indexing. Subclass that implements
+ * [PreferenceTitleProvider] / [PreferenceSummaryProvider] will not have its title / summary
+ * indexed.
+ */
+@AnyThread
+interface PreferenceMetadata {
+
+ /** Preference key. */
+ val key: String
+
+ /**
+ * Preference title resource id.
+ *
+ * Implement [PreferenceTitleProvider] if title is generated dynamically.
+ */
+ val title: Int
+ @StringRes get() = 0
+
+ /**
+ * Preference summary resource id.
+ *
+ * Implement [PreferenceSummaryProvider] if summary is generated dynamically (e.g. summary is
+ * provided per preference value)
+ */
+ val summary: Int
+ @StringRes get() = 0
+
+ /** Icon of the preference. */
+ val icon: Int
+ @DrawableRes get() = 0
+
+ /** Additional keywords for indexing. */
+ val keywords: Int
+ @StringRes get() = 0
+
+ /**
+ * Return the extras Bundle object associated with this preference.
+ *
+ * It is used to provide more information for metadata.
+ */
+ fun extras(context: Context): Bundle? = null
+
+ /**
+ * Returns if preference is indexable, default value is `true`.
+ *
+ * Return `false` only when the preference is always unavailable on current device. If it is
+ * conditional available, override [PreferenceAvailabilityProvider].
+ */
+ fun isIndexable(context: Context): Boolean = true
+
+ /**
+ * Returns if preference is enabled.
+ *
+ * UI framework normally does not allow user to interact with the preference widget when it is
+ * disabled.
+ *
+ * [dependencyOfEnabledState] is provided to support dependency, the [shouldDisableDependents]
+ * value of dependent preference is used to decide enabled state.
+ */
+ fun isEnabled(context: Context): Boolean {
+ val dependency = dependencyOfEnabledState(context) ?: return true
+ return !dependency.shouldDisableDependents(context)
+ }
+
+ /** Returns the key of depended preference to decide the enabled state. */
+ fun dependencyOfEnabledState(context: Context): PreferenceMetadata? = null
+
+ /** Returns whether this preference's dependents should be disabled. */
+ fun shouldDisableDependents(context: Context): Boolean = !isEnabled(context)
+
+ /** Returns if the preference is persistent in datastore. */
+ fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+
+ /**
+ * Returns if preference value backup is allowed (by default returns `true` if preference is
+ * persistent).
+ */
+ fun allowBackup(context: Context): Boolean = isPersistent(context)
+
+ /** Returns preference intent. */
+ fun intent(context: Context): Intent? = null
+
+ /** Returns preference order. */
+ fun order(context: Context): Int? = null
+
+ /**
+ * Returns the preference title.
+ *
+ * Implement [PreferenceTitleProvider] interface if title content is generated dynamically.
+ */
+ fun getPreferenceTitle(context: Context): CharSequence? =
+ when {
+ title != 0 -> context.getText(title)
+ this is PreferenceTitleProvider -> getTitle(context)
+ else -> null
+ }
+
+ /**
+ * Returns the preference summary.
+ *
+ * Implement [PreferenceSummaryProvider] interface if summary content is generated dynamically
+ * (e.g. summary is provided per preference value).
+ */
+ fun getPreferenceSummary(context: Context): CharSequence? =
+ when {
+ summary != 0 -> context.getText(summary)
+ this is PreferenceSummaryProvider -> getSummary(context)
+ else -> null
+ }
+}
+
+/** Metadata of preference group. */
+@AnyThread
+open class PreferenceGroup(override val key: String, override val title: Int) : PreferenceMetadata
+
+/** Metadata of preference screen. */
+@AnyThread
+interface PreferenceScreenMetadata : PreferenceMetadata {
+
+ /**
+ * The screen title resource, which precedes [getScreenTitle] if provided.
+ *
+ * By default, screen title is same with [title].
+ */
+ val screenTitle: Int
+ get() = title
+
+ /** Returns dynamic screen title, use [screenTitle] whenever possible. */
+ fun getScreenTitle(context: Context): CharSequence? = null
+
+ /** Returns the fragment class to show the preference screen. */
+ fun fragmentClass(): Class<out Fragment>?
+
+ /**
+ * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen.
+ *
+ * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen.
+ * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is
+ * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in
+ * [getPreferenceHierarchy] will only be used to bind UI widgets.
+ */
+ fun hasCompleteHierarchy(): Boolean = true
+
+ /**
+ * Returns the hierarchy of preference screen.
+ *
+ * The implementation MUST include all preferences into the hierarchy regardless of the runtime
+ * conditions. DO NOT check any condition (except compile time flag) before adding a preference.
+ */
+ fun getPreferenceHierarchy(context: Context): PreferenceHierarchy
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
new file mode 100644
index 0000000..84014f1
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+
+/** Provides the associated preference screen key for binding. */
+interface PreferenceScreenBindingKeyProvider {
+
+ /** Returns the associated preference screen key. */
+ fun getPreferenceScreenBindingKey(context: Context): String?
+}
+
+/** Extra key to provide the preference screen key for binding. */
+const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
new file mode 100644
index 0000000..48798da
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+import com.android.settingslib.datastore.KeyValueStore
+import com.google.common.base.Supplier
+import com.google.common.base.Suppliers
+import com.google.common.collect.ImmutableMap
+
+private typealias PreferenceScreenMap = ImmutableMap<String, PreferenceScreenMetadata>
+
+/** Registry of all available preference screens in the app. */
+object PreferenceScreenRegistry : ReadWritePermitProvider {
+
+ /** Provider of key-value store. */
+ private lateinit var keyValueStoreProvider: KeyValueStoreProvider
+
+ private var preferenceScreensSupplier: Supplier<PreferenceScreenMap> = Supplier {
+ ImmutableMap.of()
+ }
+
+ private val preferenceScreens: PreferenceScreenMap
+ get() = preferenceScreensSupplier.get()
+
+ private var readWritePermitProvider: ReadWritePermitProvider? = null
+
+ /** Sets the [KeyValueStoreProvider]. */
+ fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
+ this.keyValueStoreProvider = keyValueStoreProvider
+ }
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Must call [setKeyValueStoreProvider] before invoking this method, otherwise
+ * [NullPointerException] is raised.
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? =
+ keyValueStoreProvider.getKeyValueStore(context, preference)
+
+ /** Sets supplier to provide available preference screens. */
+ fun setPreferenceScreensSupplier(supplier: Supplier<List<PreferenceScreenMetadata>>) {
+ preferenceScreensSupplier =
+ Suppliers.memoize {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in supplier.get()) screensBuilder.put(screen.key, screen)
+ screensBuilder.buildOrThrow()
+ }
+ }
+
+ /** Sets available preference screens. */
+ fun setPreferenceScreens(vararg screens: PreferenceScreenMetadata) {
+ val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+ for (screen in screens) screensBuilder.put(screen.key, screen)
+ preferenceScreensSupplier = Suppliers.ofInstance(screensBuilder.buildOrThrow())
+ }
+
+ /** Returns [PreferenceScreenMetadata] of particular key. */
+ operator fun get(key: String?): PreferenceScreenMetadata? =
+ if (key != null) preferenceScreens[key] else null
+
+ /**
+ * Sets the provider to check read write permit. Read and write requests are denied by default.
+ */
+ fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider?) {
+ this.readWritePermitProvider = readWritePermitProvider
+ }
+
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getReadPermit(context, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) =
+ readWritePermitProvider?.getWritePermit(context, value, myUid, callingUid, preference)
+ ?: ReadWritePermit.DISALLOW
+}
+
+/** Provider of [KeyValueStore]. */
+fun interface KeyValueStoreProvider {
+
+ /**
+ * Returns the key-value store for given preference.
+ *
+ * Here are some use cases:
+ * - provide the default storage for all preferences
+ * - determine the storage per preference keys or the interfaces implemented by the preference
+ */
+ fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
+}
+
+/** Provider of read and write permit. */
+interface ReadWritePermitProvider {
+
+ @ReadWritePermit
+ fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ @ReadWritePermit
+ fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ): Int
+
+ companion object {
+ @JvmField
+ val ALLOW_ALL_READ_WRITE =
+ object : ReadWritePermitProvider {
+ override fun getReadPermit(
+ context: Context,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+
+ override fun getWritePermit(
+ context: Context,
+ value: Any?,
+ myUid: Int,
+ callingUid: Int,
+ preference: PreferenceMetadata,
+ ) = ReadWritePermit.ALLOW
+ }
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
new file mode 100644
index 0000000..a3aa85d
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+
+/**
+ * Interface to provide dynamic preference title.
+ *
+ * Implement this interface implies that the preference title should not be cached for indexing.
+ */
+interface PreferenceTitleProvider {
+
+ /** Provides preference title. */
+ fun getTitle(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide dynamic preference summary.
+ *
+ * Implement this interface implies that the preference summary should not be cached for indexing.
+ */
+interface PreferenceSummaryProvider {
+
+ /** Provides preference summary. */
+ fun getSummary(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide the state of preference availability.
+ *
+ * UI framework normally does not show the preference widget if it is unavailable.
+ */
+interface PreferenceAvailabilityProvider {
+
+ /** Returns if the preference is available. */
+ fun isAvailable(context: Context): Boolean
+}
+
+/**
+ * Interface to provide the managed configuration state of the preference.
+ *
+ * See [Managed configurations](https://developer.android.com/work/managed-configurations) for the
+ * Android Enterprise support.
+ */
+interface PreferenceRestrictionProvider {
+
+ /** Returns if preference is restricted by managed configs. */
+ fun isRestricted(context: Context): Boolean
+}
+
+/**
+ * Preference lifecycle to deal with preference state.
+ *
+ * Implement this interface when preference depends on runtime conditions.
+ */
+interface PreferenceLifecycleProvider {
+
+ /**
+ * Called when preference is attached to UI.
+ *
+ * Subclass could override this API to register runtime condition listeners, and invoke
+ * `onPreferenceStateChanged(this)` on the given [preferenceStateObserver] to update UI when
+ * internal state (e.g. availability, enabled state, title, summary) is changed.
+ */
+ fun onAttach(context: Context, preferenceStateObserver: PreferenceStateObserver)
+
+ /**
+ * Called when preference is detached from UI.
+ *
+ * Clean up and release resource.
+ */
+ fun onDetach(context: Context)
+
+ /** Observer of preference state. */
+ interface PreferenceStateObserver {
+
+ /** Callbacks when preference state is changed. */
+ fun onPreferenceStateChanged(preference: PreferenceMetadata)
+ }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
new file mode 100644
index 0000000..ad996c7
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.metadata
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+/**
+ * Common base class for preferences that have two selectable states, save a boolean value, and may
+ * have dependent preferences that are enabled/disabled based on the current state.
+ */
+interface TwoStatePreference : PreferenceMetadata, PersistentPreference<Boolean>, BooleanValue {
+
+ override fun shouldDisableDependents(context: Context) =
+ storage(context).getValue(key, Boolean::class.javaObjectType) != true ||
+ super.shouldDisableDependents(context)
+}
+
+/** A preference that provides a two-state toggleable option. */
+open class SwitchPreference
+@JvmOverloads
+constructor(
+ override val key: String,
+ @StringRes override val title: Int = 0,
+ @StringRes override val summary: Int = 0,
+) : TwoStatePreference
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
new file mode 100644
index 0000000..9665dbd
--- /dev/null
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -0,0 +1,23 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibPreference-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibPreference",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibPreference-srcs"],
+ static_libs: [
+ "SettingsLibDataStore",
+ "SettingsLibMetadata",
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml
new file mode 100644
index 0000000..2d7f7ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.preference">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
new file mode 100644
index 0000000..9be0e71
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.DialogPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SeekBarPreference
+import com.android.settingslib.metadata.DiscreteIntValue
+import com.android.settingslib.metadata.DiscreteValue
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.RangeValue
+
+/** Binding of preference widget and preference metadata. */
+interface PreferenceBinding {
+
+ /**
+ * Provides a new [Preference] widget instance.
+ *
+ * By default, it returns a new [Preference] object. Subclass could override this method to
+ * provide customized widget and do **one-off** initialization (e.g.
+ * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed,
+ * override the [bind] method.
+ *
+ * Notes:
+ * - DO NOT set any properties defined in [PreferenceMetadata]. For example,
+ * title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties
+ * will be reset by [bind].
+ * - Override [bind] if needed to provide more information for customized widget.
+ */
+ fun createWidget(context: Context): Preference = Preference(context)
+
+ /**
+ * Binds preference widget with given metadata.
+ *
+ * Whenever metadata state is changed, this callback is invoked to update widget. By default,
+ * the common states like title, summary, enabled, etc. are already applied. Subclass should
+ * override this method to bind more data (e.g. read preference value from storage and apply it
+ * to widget).
+ *
+ * @param preference preference widget created by [createWidget]
+ * @param metadata metadata to apply
+ */
+ fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ metadata.apply {
+ preference.key = key
+ if (icon != 0) {
+ preference.setIcon(icon)
+ } else {
+ preference.icon = null
+ }
+ val context = preference.context
+ preference.peekExtras()?.clear()
+ extras(context)?.let { preference.extras.putAll(it) }
+ preference.title = getPreferenceTitle(context)
+ preference.summary = getPreferenceSummary(context)
+ preference.isEnabled = isEnabled(context)
+ preference.isVisible =
+ (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
+ preference.isPersistent = isPersistent(context)
+ metadata.order(context)?.let { preference.order = it }
+ // PreferenceRegistry will notify dependency change, so we do not need to set
+ // dependency here. This simplifies dependency management and avoid the
+ // IllegalStateException when call Preference.setDependency
+ preference.dependency = null
+ if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+ preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
+ preference.intent = intent(context)
+ }
+ if (preference is DialogPreference) {
+ preference.dialogTitle = preference.title
+ }
+ if (preference is ListPreference && this is DiscreteValue<*>) {
+ preference.setEntries(valuesDescription)
+ if (this is DiscreteIntValue) {
+ val intValues = context.resources.getIntArray(values)
+ preference.entryValues = Array(intValues.size) { intValues[it].toString() }
+ } else {
+ preference.setEntryValues(values)
+ }
+ } else if (preference is SeekBarPreference && this is RangeValue) {
+ preference.min = minValue
+ preference.max = maxValue
+ preference.seekBarIncrement = incrementStep
+ }
+ }
+ }
+}
+
+/** Abstract preference screen to provide preference hierarchy and binding factory. */
+interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {
+
+ val preferenceBindingFactory: PreferenceBindingFactory
+ get() = DefaultPreferenceBindingFactory
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory) =
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context))
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
new file mode 100644
index 0000000..4c2e1ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.preference
+
+import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.SwitchPreference
+
+/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
+interface PreferenceBindingFactory {
+
+ /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
+ fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
+}
+
+/** Default [PreferenceBindingFactory]. */
+object DefaultPreferenceBindingFactory : PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ metadata as? PreferenceBinding
+ ?: when (metadata) {
+ is SwitchPreference -> SwitchPreferenceBinding.INSTANCE
+ is PreferenceGroup -> PreferenceGroupBinding.INSTANCE
+ is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE
+ else -> DefaultPreferenceBinding
+ }
+}
+
+/** A preference key based binding factory. */
+class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) :
+ PreferenceBindingFactory {
+
+ override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+ bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
new file mode 100644
index 0000000..ede970e
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceTitleProvider
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceScreenBinding : PreferenceBinding {
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ val context = preference.context
+ val screenMetadata = metadata as PreferenceScreenMetadata
+ // Pass the preference key to fragment, so that the fragment could find associated
+ // preference screen registered in PreferenceScreenRegistry
+ preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+ if (preference is PreferenceScreen) {
+ val screenTitle = screenMetadata.screenTitle
+ preference.title =
+ if (screenTitle != 0) {
+ context.getString(screenTitle)
+ } else {
+ screenMetadata.getScreenTitle(context)
+ ?: (this as? PreferenceTitleProvider)?.getTitle(context)
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceScreenBinding {}
+ }
+}
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceGroupBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context) = PreferenceCategory(context)
+
+ companion object {
+ @JvmStatic val INSTANCE = object : PreferenceGroupBinding {}
+ }
+}
+
+/** A boolean value type preference associated with [SwitchPreferenceCompat]. */
+interface SwitchPreferenceBinding : PreferenceBinding {
+
+ override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context)
+
+ override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+ super.bind(preference, metadata)
+ (metadata as? PersistentPreference<*>)
+ ?.storage(preference.context)
+ ?.getValue(metadata.key, Boolean::class.javaObjectType)
+ ?.let { (preference as SwitchPreferenceCompat).isChecked = it }
+ }
+
+ companion object {
+ @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {}
+ }
+}
+
+/** Default [PreferenceBinding] for [Preference]. */
+object DefaultPreferenceBinding : PreferenceBinding
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
new file mode 100644
index 0000000..02acfca
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */
+class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() {
+
+ override fun getBoolean(key: String, defValue: Boolean): Boolean =
+ keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue
+
+ override fun getFloat(key: String, defValue: Float): Float =
+ keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue
+
+ override fun getInt(key: String, defValue: Int): Int =
+ keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue
+
+ override fun getLong(key: String, defValue: Long): Long =
+ keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue
+
+ override fun getString(key: String, defValue: String?): String? =
+ keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+
+ override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
+ (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
+
+ override fun putBoolean(key: String, value: Boolean) =
+ keyValueStore.setValue(key, Boolean::class.javaObjectType, value)
+
+ override fun putFloat(key: String, value: Float) =
+ keyValueStore.setValue(key, Float::class.javaObjectType, value)
+
+ override fun putInt(key: String, value: Int) =
+ keyValueStore.setValue(key, Int::class.javaObjectType, value)
+
+ override fun putLong(key: String, value: Long) =
+ keyValueStore.setValue(key, Long::class.javaObjectType, value)
+
+ override fun putString(key: String, value: String?) =
+ keyValueStore.setValue(key, String::class.javaObjectType, value)
+
+ override fun putStringSet(key: String, values: Set<String>?) =
+ keyValueStore.setValue(key, Set::class.javaObjectType, values)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
new file mode 100644
index 0000000..2072009
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.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.preference
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.XmlRes
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
+
+/** Fragment to display a preference screen. */
+open class PreferenceFragment :
+ PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
+
+ private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ preferenceScreen = createPreferenceScreen()
+ }
+
+ fun createPreferenceScreen(): PreferenceScreen? =
+ createPreferenceScreen(PreferenceScreenFactory(this))
+
+ override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? {
+ val context = factory.context
+ fun createPreferenceScreenFromResource() =
+ factory.inflate(getPreferenceScreenResId(context))
+
+ if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource()
+
+ val screenKey = getPreferenceScreenBindingKey(context)
+ val screenCreator =
+ (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator)
+ ?: return createPreferenceScreenFromResource()
+
+ val preferenceBindingFactory = screenCreator.preferenceBindingFactory
+ val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context)
+ val preferenceScreen =
+ if (screenCreator.hasCompleteHierarchy()) {
+ factory.getOrCreatePreferenceScreen().apply {
+ inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
+ }
+ } else {
+ createPreferenceScreenFromResource()?.also {
+ bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
+ } ?: return null
+ }
+ preferenceScreenBindingHelper =
+ PreferenceScreenBindingHelper(
+ context,
+ preferenceBindingFactory,
+ preferenceScreen,
+ preferenceHierarchy,
+ )
+ return preferenceScreen
+ }
+
+ /**
+ * Returns if preference screen metadata can be used to set up preference screen.
+ *
+ * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build
+ * preference screen.
+ */
+ protected open fun usePreferenceScreenMetadata(): Boolean = true
+
+ /** Returns the xml resource to create preference screen. */
+ @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
+
+ override fun getPreferenceScreenBindingKey(context: Context): String? =
+ arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+
+ override fun onDestroy() {
+ preferenceScreenBindingHelper?.close()
+ super.onDestroy()
+ }
+
+ companion object {
+ /** Returns [PreferenceFragment] instance to display the preference screen of given key. */
+ fun of(screenKey: String): PreferenceFragment? {
+ val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (
+ screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy()
+ ) {
+ return PreferenceFragment().apply {
+ arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) }
+ }
+ }
+ return null
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
new file mode 100644
index 0000000..5ef7823
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import androidx.preference.PreferenceGroup
+import com.android.settingslib.datastore.KeyValueStore
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceMetadata
+
+/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
+fun PreferenceGroup.inflatePreferenceHierarchy(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ hierarchy: PreferenceHierarchy,
+ storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
+) {
+ fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
+
+ hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+ hierarchy.forEach {
+ val metadata = it.metadata
+ val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+ val widget = preferenceBinding.createWidget(context)
+ if (it is PreferenceHierarchy) {
+ val preferenceGroup = widget as PreferenceGroup
+ // MUST add preference before binding, otherwise exception is raised when add child
+ addPreference(preferenceGroup)
+ preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
+ } else {
+ preferenceBinding.bind(widget, metadata)
+ (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+ widget.preferenceDataStore =
+ storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
+ }
+ // MUST add preference after binding for persistent preference to get initial value
+ // (preference key is set within bind method)
+ addPreference(widget)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
new file mode 100644
index 0000000..3610894
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.KeyedDataObservable
+import com.android.settingslib.datastore.KeyedObservable
+import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableMultimap
+import java.util.concurrent.Executor
+
+/**
+ * Helper to bind preferences on given [preferenceScreen].
+ *
+ * When there is any preference change event detected (e.g. preference value changed, runtime
+ * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update
+ * widget UI.
+ */
+class PreferenceScreenBindingHelper(
+ context: Context,
+ private val preferenceBindingFactory: PreferenceBindingFactory,
+ private val preferenceScreen: PreferenceScreen,
+ preferenceHierarchy: PreferenceHierarchy,
+) : KeyedDataObservable<String>(), AutoCloseable {
+
+ private val handler = Handler(Looper.getMainLooper())
+ private val executor =
+ object : Executor {
+ override fun execute(command: Runnable) {
+ handler.post(command)
+ }
+ }
+
+ private val preferences: ImmutableMap<String, PreferenceMetadata>
+ private val dependencies: ImmutableMultimap<String, String>
+ private val storages = mutableSetOf<KeyedObservable<String>>()
+
+ private val preferenceObserver: KeyedObserver<String?>
+
+ private val storageObserver =
+ KeyedObserver<String?> { key, _ ->
+ if (key != null) {
+ notifyChange(key, CHANGE_REASON_VALUE)
+ }
+ }
+
+ private val stateObserver =
+ object : PreferenceLifecycleProvider.PreferenceStateObserver {
+ override fun onPreferenceStateChanged(preference: PreferenceMetadata) {
+ notifyChange(preference.key, CHANGE_REASON_STATE)
+ }
+ }
+
+ init {
+ val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+ val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
+ fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
+ dependenciesBuilder.put(key, dependency.key)
+ }
+
+ fun PreferenceMetadata.add() {
+ preferencesBuilder.put(key, this)
+ dependencyOfEnabledState(context)?.addDependency(this)
+ if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver)
+ if (this is PersistentPreference<*>) storages.add(storage(context))
+ }
+
+ fun PreferenceHierarchy.addPreferences() {
+ metadata.add()
+ forEach {
+ if (it is PreferenceHierarchy) {
+ it.addPreferences()
+ } else {
+ it.metadata.add()
+ }
+ }
+ }
+
+ preferenceHierarchy.addPreferences()
+ this.preferences = preferencesBuilder.buildOrThrow()
+ this.dependencies = dependenciesBuilder.build()
+
+ preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
+ addObserver(preferenceObserver, executor)
+ for (storage in storages) storage.addObserver(storageObserver, executor)
+ }
+
+ private fun onPreferenceChange(key: String?, reason: Int) {
+ if (key == null) return
+
+ // bind preference to update UI
+ preferenceScreen.findPreference<Preference>(key)?.let {
+ preferenceBindingFactory.bind(it, preferences[key])
+ }
+
+ // check reason to avoid potential infinite loop
+ if (reason != CHANGE_REASON_DEPENDENT) {
+ notifyDependents(key, mutableSetOf())
+ }
+ }
+
+ /** Notifies dependents recursively. */
+ private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
+ if (!notifiedKeys.add(key)) return
+ for (dependency in dependencies[key]) {
+ notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+ notifyDependents(dependency, notifiedKeys)
+ }
+ }
+
+ override fun close() {
+ removeObserver(preferenceObserver)
+ val context = preferenceScreen.context
+ for (preference in preferences.values) {
+ if (preference is PreferenceLifecycleProvider) preference.onDetach(context)
+ }
+ for (storage in storages) storage.removeObserver(storageObserver)
+ }
+
+ companion object {
+ /** Preference value is changed. */
+ private const val CHANGE_REASON_VALUE = 0
+ /** Preference state (title/summary, enable state, etc.) is changed. */
+ private const val CHANGE_REASON_STATE = 1
+ /** Dependent preference state is changed. */
+ private const val CHANGE_REASON_DEPENDENT = 2
+
+ /** Updates preference screen that has incomplete hierarchy. */
+ @JvmStatic
+ fun bind(preferenceScreen: PreferenceScreen) {
+ PreferenceScreenRegistry[preferenceScreen.key]?.run {
+ if (!hasCompleteHierarchy()) {
+ val preferenceBindingFactory =
+ (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
+ bindRecursively(
+ preferenceScreen,
+ preferenceBindingFactory,
+ getPreferenceHierarchy(preferenceScreen.context),
+ )
+ }
+ }
+ }
+
+ internal fun bindRecursively(
+ preferenceScreen: PreferenceScreen,
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferenceHierarchy: PreferenceHierarchy,
+ ) =
+ preferenceScreen.bindRecursively(
+ preferenceBindingFactory,
+ preferenceHierarchy.getAllPreferences().associateBy { it.key },
+ )
+
+ private fun PreferenceGroup.bindRecursively(
+ preferenceBindingFactory: PreferenceBindingFactory,
+ preferences: Map<String, PreferenceMetadata>,
+ ) {
+ preferenceBindingFactory.bind(this, preferences[key])
+ val count = preferenceCount
+ for (index in 0 until count) {
+ val preference = getPreference(index)
+ if (preference is PreferenceGroup) {
+ preference.bindRecursively(preferenceBindingFactory, preferences)
+ } else {
+ preferenceBindingFactory.bind(preference, preferences[preference.key])
+ }
+ }
+ }
+
+ private fun PreferenceBindingFactory.bind(
+ preference: Preference,
+ metadata: PreferenceMetadata?,
+ ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
new file mode 100644
index 0000000..7f99d7a
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+
+/** Factory to create preference screen. */
+class PreferenceScreenFactory {
+ /** Preference manager to create/inflate preference screen. */
+ val preferenceManager: PreferenceManager
+
+ /**
+ * Optional existing hierarchy to merge the new hierarchies into.
+ *
+ * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for
+ * [PreferenceFragmentCompat].
+ */
+ private val rootScreen: PreferenceScreen?
+
+ /**
+ * Factory constructor from preference fragment.
+ *
+ * The fragment must be within a valid lifecycle.
+ */
+ constructor(preferenceFragment: PreferenceFragmentCompat) {
+ preferenceManager = preferenceFragment.preferenceManager
+ rootScreen = preferenceFragment.preferenceScreen
+ }
+
+ /** Factory constructor from [Context]. */
+ constructor(context: Context) : this(PreferenceManager(context))
+
+ /** Factory constructor from [PreferenceManager]. */
+ constructor(preferenceManager: PreferenceManager) {
+ this.preferenceManager = preferenceManager
+ rootScreen = null
+ }
+
+ /** Context of the factory to create preference screen. */
+ val context: Context
+ get() = preferenceManager.context
+
+ /** Returns the existing hierarchy or create a new empty preference screen. */
+ fun getOrCreatePreferenceScreen(): PreferenceScreen =
+ rootScreen ?: preferenceManager.createPreferenceScreen(context)
+
+ /**
+ * Inflates [PreferenceScreen] from xml resource.
+ *
+ * @param xmlRes The resource ID of the XML to inflate
+ * @return The root hierarchy (if one was not provided, the new hierarchy's root)
+ */
+ fun inflate(xmlRes: Int): PreferenceScreen? =
+ if (xmlRes != 0) {
+ preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen)
+ } else {
+ rootScreen
+ }
+
+ /**
+ * Creates [PreferenceScreen] of given key.
+ *
+ * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
+ */
+ fun createBindingScreen(screenKey: String?): PreferenceScreen? {
+ val metadata = PreferenceScreenRegistry[screenKey] ?: return null
+ if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
+ return metadata.createPreferenceScreen(this)
+ }
+ return null
+ }
+
+ companion object {
+ /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */
+ @JvmStatic
+ fun createBindingScreen(preference: Preference): PreferenceScreen? {
+ val preferenceScreenCreator =
+ (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator)
+ ?: return null
+ if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
+ val factory = PreferenceScreenFactory(preference.context)
+ val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory)
+ factory.preferenceManager.setPreferences(preferenceScreen)
+ return preferenceScreen
+ }
+ }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
new file mode 100644
index 0000000..0573292
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.PreferenceScreen
+
+/**
+ * Interface to provide [PreferenceScreen].
+ *
+ * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`,
+ * `getActivity()`) MUST not be used: preference screen creation could happen in background service,
+ * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked
+ * and context APIs return null.
+ */
+interface PreferenceScreenProvider {
+
+ /**
+ * Creates [PreferenceScreen].
+ *
+ * Preference screen creation could happen in background service. The implementation MUST use
+ * [PreferenceScreenFactory.context] to obtain context.
+ */
+ fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen?
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
new file mode 100644
index 0000000..65adec4
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.bluetooth.devicesettings
+
+/** The contract between the device settings provider services and Settings. */
+object DeviceSettingContract {
+ const val INVISIBLE_PROFILES = "INVISIBLE_PROFILES"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 457d6a3..769b6e6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -22,6 +22,7 @@
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference
import com.android.settingslib.bluetooth.devicesettings.DeviceSetting
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingContract
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
@@ -30,6 +31,9 @@
import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.AppProvidedItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
@@ -103,9 +107,18 @@
private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel {
return if (!TextUtils.isEmpty(preferenceKey)) {
- DeviceSettingConfigItemModel.BuiltinItem(settingId, preferenceKey!!)
+ if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
+ BluetoothProfilesItem(
+ settingId,
+ preferenceKey!!,
+ extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES)
+ ?: emptyList()
+ )
+ } else {
+ CommonBuiltinItem(settingId, preferenceKey!!)
+ }
} else {
- DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+ AppProvidedItem(settingId)
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 33beb06..7eae5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -23,6 +23,7 @@
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
+import android.text.TextUtils
import android.util.Log
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -84,6 +85,10 @@
}
setAction(intentAction)
}
+
+ fun isValid(): Boolean {
+ return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
+ }
}
private var isServiceEnabled =
@@ -96,7 +101,8 @@
} else if (allStatus.all { it is ServiceConnectionStatus.Connected }) {
allStatus
.filterIsInstance<
- ServiceConnectionStatus.Connected<IDeviceSettingsProviderService>
+ ServiceConnectionStatus.Connected<
+ IDeviceSettingsProviderService>
>()
.all { it.service.serviceStatus?.enabled == true }
} else {
@@ -215,6 +221,7 @@
)
}
}
+ ?.filter { it.isValid() }
?.distinct()
?.associateBy(
{ it },
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
index c1ac763..08fb3fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
@@ -36,10 +36,23 @@
@DeviceSettingId val settingId: Int
/** A built-in item in Settings. */
- data class BuiltinItem(
- @DeviceSettingId override val settingId: Int,
- val preferenceKey: String?
- ) : DeviceSettingConfigItemModel
+ sealed interface BuiltinItem : DeviceSettingConfigItemModel {
+ @DeviceSettingId override val settingId: Int
+ val preferenceKey: String
+
+ /** A general built-in item in Settings. */
+ data class CommonBuiltinItem(
+ @DeviceSettingId override val settingId: Int,
+ override val preferenceKey: String,
+ ) : BuiltinItem
+
+ /** A bluetooth profiles in Settings. */
+ data class BluetoothProfilesItem(
+ @DeviceSettingId override val settingId: Int,
+ override val preferenceKey: String,
+ val invisibleProfiles: List<String>,
+ ) : BuiltinItem
+ }
/** A remote item provided by other apps. */
data class AppProvidedItem(@DeviceSettingId override val settingId: Int) :
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index ce155b5..81b5634 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -91,7 +91,9 @@
`when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
`when`(
bluetoothDevice.getMetadata(
- DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+ )
+ )
.thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())
`when`(configService.queryLocalInterface(anyString())).thenReturn(configService)
@@ -114,7 +116,8 @@
connection.onServiceConnected(
ComponentName(
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
- SETTING_PROVIDER_SERVICE_CLASS_NAME_1),
+ SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
+ ),
settingProviderService1,
)
SETTING_PROVIDER_SERVICE_INTENT_ACTION_2 ->
@@ -146,16 +149,24 @@
fun getDeviceSettingsConfig_withMetadata_success() {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
assertConfig(config!!, DEVICE_SETTING_CONFIG)
+ assertThat(config.mainItems[0])
+ .isInstanceOf(DeviceSettingConfigItemModel.AppProvidedItem::class.java)
+ assertThat(config.mainItems[1])
+ .isInstanceOf(
+ DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem::class.java
+ )
+ assertThat(config.mainItems[2])
+ .isInstanceOf(
+ DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem::class.java
+ )
}
}
@@ -163,16 +174,16 @@
fun getDeviceSettingsConfig_noMetadata_returnNull() {
testScope.runTest {
`when`(
- bluetoothDevice.getMetadata(
- DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+ bluetoothDevice.getMetadata(
+ DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+ )
+ )
.thenReturn("".toByteArray())
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -184,12 +195,10 @@
fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(false)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(false))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
val config = underTest.getDeviceSettingsConfig(cachedDevice)
@@ -219,12 +228,10 @@
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -247,12 +254,10 @@
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -270,17 +275,15 @@
testScope.runTest {
`when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
`when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
- input ->
+ input ->
input
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -324,12 +327,10 @@
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -347,8 +348,10 @@
DeviceSettingState.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
.setPreferenceState(
- ActionSwitchPreferenceState.Builder().setChecked(false).build())
- .build())
+ ActionSwitchPreferenceState.Builder().setChecked(false).build()
+ )
+ .build(),
+ )
}
}
@@ -362,12 +365,10 @@
.getArgument<IDeviceSettingsListener>(1)
.onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
}
- `when`(settingProviderService1.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
- `when`(settingProviderService2.serviceStatus).thenReturn(
- DeviceSettingsProviderServiceStatus(true)
- )
+ `when`(settingProviderService1.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
+ `when`(settingProviderService2.serviceStatus)
+ .thenReturn(DeviceSettingsProviderServiceStatus(true))
var setting: DeviceSettingModel? = null
underTest
@@ -385,8 +386,10 @@
DeviceSettingState.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC)
.setPreferenceState(
- MultiTogglePreferenceState.Builder().setState(2).build())
- .build())
+ MultiTogglePreferenceState.Builder().setState(2).build()
+ )
+ .build(),
+ )
}
}
@@ -437,7 +440,7 @@
private fun assertConfig(
actual: DeviceSettingConfigModel,
- serviceResponse: DeviceSettingsConfig
+ serviceResponse: DeviceSettingsConfig,
) {
assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size)
for (i in 0..<actual.mainItems.size) {
@@ -451,7 +454,7 @@
private fun assertConfigItem(
actual: DeviceSettingConfigItemModel,
- serviceResponse: DeviceSettingItem
+ serviceResponse: DeviceSettingItem,
) {
assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
}
@@ -485,24 +488,43 @@
"</DEVICE_SETTINGS_CONFIG_ACTION>"
val DEVICE_INFO = DeviceInfo.Builder().setBluetoothAddress(BLUETOOTH_ADDRESS).build()
const val DEVICE_SETTING_ID_HELP = 12345
- val DEVICE_SETTING_ITEM_1 =
+ val DEVICE_SETTING_APP_PROVIDED_ITEM_1 =
DeviceSettingItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_1)
- val DEVICE_SETTING_ITEM_2 =
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_1,
+ )
+ val DEVICE_SETTING_APP_PROVIDED_ITEM_2 =
DeviceSettingItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+ )
+ val DEVICE_SETTING_BUILT_IN_ITEM =
+ DeviceSettingItem(
+ DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP,
+ "",
+ "",
+ "",
+ "device_type",
+ )
+ val DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM =
+ DeviceSettingItem(
+ DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
+ "",
+ "",
+ "",
+ "bluetooth_profiles",
+ )
val DEVICE_SETTING_HELP_ITEM =
DeviceSettingItem(
DEVICE_SETTING_ID_HELP,
SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
- SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+ SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+ )
val DEVICE_SETTING_1 =
DeviceSetting.Builder()
.setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
@@ -511,7 +533,8 @@
.setTitle("title1")
.setHasSwitch(true)
.setAllowedChangingState(true)
- .build())
+ .build()
+ )
.build()
val DEVICE_SETTING_2 =
DeviceSetting.Builder()
@@ -524,22 +547,30 @@
ToggleInfo.Builder()
.setLabel("label1")
.setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- .build())
+ .build()
+ )
.addToggleInfo(
ToggleInfo.Builder()
.setLabel("label2")
.setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
- .build())
- .build())
+ .build()
+ )
+ .build()
+ )
.build()
- val DEVICE_SETTING_HELP = DeviceSetting.Builder()
- .setSettingId(DEVICE_SETTING_ID_HELP)
- .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
- .build()
+ val DEVICE_SETTING_HELP =
+ DeviceSetting.Builder()
+ .setSettingId(DEVICE_SETTING_ID_HELP)
+ .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
+ .build()
val DEVICE_SETTING_CONFIG =
DeviceSettingsConfig(
- listOf(DEVICE_SETTING_ITEM_1),
- listOf(DEVICE_SETTING_ITEM_2),
+ listOf(
+ DEVICE_SETTING_APP_PROVIDED_ITEM_1,
+ DEVICE_SETTING_BUILT_IN_ITEM,
+ DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM,
+ ),
+ listOf(DEVICE_SETTING_APP_PROVIDED_ITEM_2),
DEVICE_SETTING_HELP_ITEM,
)
}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d26a906..a9e81c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -756,6 +756,7 @@
"notification_flags_lib",
"PlatformComposeCore",
"PlatformComposeSceneTransitionLayout",
+ "PlatformComposeSceneTransitionLayoutTestsUtils",
"androidx.compose.runtime_runtime",
"androidx.compose.material3_material3",
"androidx.compose.material_material-icons-extended",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 892f778..7c89592 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -288,6 +288,16 @@
}
flag {
+ name: "qs_quick_rebind_active_tiles"
+ namespace: "systemui"
+ description: "Rebind active custom tiles quickly."
+ bug: "362526228"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "coroutine_tracing"
namespace: "systemui"
description: "Adds thread-local data to System UI's global coroutine scopes to "
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7fb88e8..ae92d259 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,8 +99,8 @@
BouncerContent(
viewModel,
dialogFactory,
- Modifier.sysuiResTag(Bouncer.TestTags.Root)
- .element(Bouncer.Elements.Content)
+ Modifier.element(Bouncer.Elements.Content)
+ .sysuiResTag(Bouncer.TestTags.Root)
.fillMaxSize()
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 3cb0d8a..df101c5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -128,7 +128,11 @@
}
},
) {
- SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
+ SceneTransitionLayout(
+ state = state,
+ modifier = modifier.fillMaxSize(),
+ swipeSourceDetector = viewModel.edgeDetector,
+ ) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
key = sceneKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f3577fa..007b84a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -395,14 +395,8 @@
return 0f
}
- fun animateTo(targetContent: T) {
- swipeAnimation.animateOffset(
- initialVelocity = velocity,
- targetContent = targetContent,
- )
- }
-
val fromContent = swipeAnimation.fromContent
+ val consumedVelocity: Float
if (canChangeContent) {
// If we are halfway between two contents, we check what the target will be based on the
// velocity and offset of the transition, then we launch the animation.
@@ -427,18 +421,16 @@
} else {
fromContent
}
-
- animateTo(targetContent = targetContent)
+ consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = targetContent)
} else {
// We are doing an overscroll preview animation between scenes.
check(fromContent == swipeAnimation.currentContent) {
"canChangeContent is false but currentContent != fromContent"
}
- animateTo(targetContent = fromContent)
+ consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = fromContent)
}
- // The onStop animation consumes any remaining velocity.
- return velocity
+ return consumedVelocity
}
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 2a09a77..966bda4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -312,11 +312,16 @@
fun isAnimatingOffset(): Boolean = offsetAnimation != null
+ /**
+ * Animate the offset to a [targetContent], using the [initialVelocity] and an optional [spec]
+ *
+ * @return the velocity consumed
+ */
fun animateOffset(
initialVelocity: Float,
targetContent: T,
spec: AnimationSpec<Float>? = null,
- ) {
+ ): Float {
check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }
val initialProgress = progress
@@ -374,7 +379,7 @@
if (skipAnimation) {
// Unblock the job.
offsetAnimationRunnable.complete(null)
- return
+ return 0f
}
val isTargetGreater = targetOffset > animatable.value
@@ -424,6 +429,9 @@
/* Ignore. */
}
}
+
+ // This animation always consumes the whole available velocity
+ return initialVelocity
}
/** An exception thrown during the animation to stop it immediately. */
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 79f82c9..5b59356 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1111,7 +1111,7 @@
assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
// Release the finger.
- dragController.onDragStopped(velocity = -velocityThreshold)
+ dragController.onDragStopped(velocity = -velocityThreshold, expectedConsumed = false)
// Exhaust all coroutines *without advancing the clock*. Given that we are at progress >=
// 100% and that the overscroll on scene B is doing nothing, we are already idle.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index e2bdc49..bb15208 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,10 +30,12 @@
import com.android.systemui.classifier.FalsingCollectorFake
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.testKosmos
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
@@ -89,6 +91,9 @@
@Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
+ private val kosmos = testKosmos()
+ private val msdlPlayer = kosmos.msdlPlayer
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -112,7 +117,8 @@
mKeyguardMessageAreaControllerFactory,
mPostureController,
fakeFeatureFlags,
- mSelectedUserInteractor
+ mSelectedUserInteractor,
+ msdlPlayer,
)
mKeyguardPatternView.onAttachedToWindow()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
index 8f9b7c8..12c866f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
@@ -30,11 +30,11 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.SysuiTestCase;
import org.junit.Before;
@@ -48,7 +48,7 @@
@RunWith(AndroidJUnit4.class)
public class MirrorWindowControlTest extends SysuiTestCase {
- @Mock WindowManager mWindowManager;
+ @Mock ViewCaptureAwareWindowManager mWindowManager;
View mView;
int mViewWidth;
int mViewHeight;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
index dd85d9b..fc57757 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
@@ -20,11 +20,15 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.userRepository
+import com.android.systemui.user.utils.FakeUserScopedService
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -39,10 +43,11 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@Suppress("UnspecifiedRegisterReceiverFlag")
@RunWith(AndroidJUnit4::class)
class CaptioningRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
@Captor
private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener>
@@ -50,34 +55,33 @@
private lateinit var underTest: CaptioningRepository
- private val testScope = TestScope()
-
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
underTest =
- CaptioningRepositoryImpl(
- captioningManager,
- testScope.testScheduler,
- testScope.backgroundScope
- )
+ with(kosmos) {
+ CaptioningRepositoryImpl(
+ FakeUserScopedService(captioningManager),
+ userRepository,
+ testScope.testScheduler,
+ applicationCoroutineScope,
+ )
+ }
}
@Test
fun isSystemAudioCaptioningEnabled_change_repositoryEmits() {
- testScope.runTest {
- `when`(captioningManager.isEnabled).thenReturn(false)
- val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>()
- underTest.isSystemAudioCaptioningEnabled
- .onEach { isSystemAudioCaptioningEnabled.add(it) }
- .launchIn(backgroundScope)
+ kosmos.testScope.runTest {
+ `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(false)
+ val models by collectValues(underTest.captioningModel.filterNotNull())
runCurrent()
+ `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(true)
triggerOnSystemAudioCaptioningChange()
runCurrent()
- assertThat(isSystemAudioCaptioningEnabled)
+ assertThat(models.map { it.isSystemAudioCaptioningEnabled })
.containsExactlyElementsIn(listOf(false, true))
.inOrder()
}
@@ -85,18 +89,16 @@
@Test
fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() {
- testScope.runTest {
- `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false)
- val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>()
- underTest.isSystemAudioCaptioningUiEnabled
- .onEach { isSystemAudioCaptioningUiEnabled.add(it) }
- .launchIn(backgroundScope)
+ kosmos.testScope.runTest {
+ `when`(captioningManager.isEnabled).thenReturn(false)
+ val models by collectValues(underTest.captioningModel.filterNotNull())
runCurrent()
+ `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(true)
triggerSystemAudioCaptioningUiChange()
runCurrent()
- assertThat(isSystemAudioCaptioningUiEnabled)
+ assertThat(models.map { it.isSystemAudioCaptioningUiEnabled })
.containsExactlyElementsIn(listOf(false, true))
.inOrder()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index 3b0057d..e531e65 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -22,6 +22,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -73,6 +74,7 @@
keyguardInteractor = kosmos.keyguardInteractor,
keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
dreamManager = dreamManager,
+ communalSceneInteractor = kosmos.communalSceneInteractor,
bgScope = kosmos.applicationCoroutineScope,
)
.apply { start() }
@@ -158,6 +160,36 @@
}
}
+ @Test
+ fun shouldNotStartDreamWhenLaunchingWidget() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setDreaming(false)
+ powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+ kosmos.communalSceneInteractor.setIsLaunchingWidget(true)
+ whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+ runCurrent()
+
+ transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+ verify(dreamManager, never()).startDream()
+ }
+
+ @Test
+ fun shouldNotStartDreamWhenOccluded() =
+ testScope.runTest {
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setDreaming(false)
+ powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+ keyguardRepository.setKeyguardOccluded(true)
+ whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+ runCurrent()
+
+ transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+ verify(dreamManager, never()).startDream()
+ }
+
private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = from,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
index c1dcf37..69ccc58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
@@ -41,7 +41,6 @@
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.testKosmos
@@ -86,7 +85,6 @@
context,
testDispatcher,
testScope,
- kosmos.activityStarter,
mediaControllerFactory,
mediaFlags,
kosmos.imageLoader,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4b132c4..a0bb017 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,9 +18,12 @@
package com.android.systemui.scene.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.fakeFalsingManager
@@ -37,6 +40,10 @@
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -60,6 +67,7 @@
private val testScope by lazy { kosmos.testScope }
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
private val falsingManager by lazy { kosmos.fakeFalsingManager }
@@ -75,6 +83,8 @@
sceneInteractor = sceneInteractor,
falsingInteractor = kosmos.falsingInteractor,
powerInteractor = kosmos.powerInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
+ splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
@@ -287,4 +297,48 @@
assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade)
}
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_singleShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_splitShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
new file mode 100644
index 0000000..3d76d28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SplitEdgeDetectorTest : SysuiTestCase() {
+
+ private val edgeSize = 40
+ private val screenWidth = 800
+ private val screenHeight = 600
+
+ private var edgeSplitFraction = 0.7f
+
+ private val underTest =
+ SplitEdgeDetector(
+ topEdgeSplitFraction = { edgeSplitFraction },
+ edgeSize = edgeSize.dp,
+ )
+
+ @Test
+ fun source_noEdge_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 2,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopRight_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() + 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_edgeSplitFractionUpdatesDynamically() {
+ val middleX = (screenWidth * 0.5f).toInt()
+ val topY = 0
+
+ // Split closer to the right; middle of screen is considered "left".
+ edgeSplitFraction = 0.6f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
+
+ // Split closer to the left; middle of screen is considered "right".
+ edgeSplitFraction = 0.4f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
+
+ // Illegal fraction.
+ edgeSplitFraction = 1.2f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+
+ // Illegal fraction.
+ edgeSplitFraction = -0.3f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+ }
+
+ @Test
+ fun source_swipeVerticallyOnBottom_detectsBottom() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize / 2),
+ )
+ assertThat(detectedEdge).isEqualTo(Bottom)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnBottom_detectsNothing() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize - 1),
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnLeft_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnRight_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun resolve_startInLtr_resolvesLeft() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_startInRtl_resolvesRight() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInLtr_resolvesRight() {
+ val resolvedEdge = End.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInRtl_resolvesLeft() {
+ val resolvedEdge = End.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_topStartInLtr_resolvesTopLeft() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun resolve_topStartInRtl_resolvesTopRight() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInLtr_resolvesTopRight() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInRtl_resolvesTopLeft() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Vertical)
+ }
+
+ private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Horizontal)
+ }
+
+ private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
+ return underTest.source(
+ layoutSize = IntSize(width = screenWidth, height = screenHeight),
+ position = IntOffset(x, y),
+ density = Density(1f),
+ orientation = orientation,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 3283ea1..9464c75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -24,7 +24,6 @@
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,6 +38,7 @@
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.shade.shared.flag.DualShade
@@ -66,18 +66,18 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
- val kosmos = testKosmos()
- val testScope = kosmos.testScope
- val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
- val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
- val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
- val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
- val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
- val powerRepository by lazy { kosmos.fakePowerRepository }
- val shadeTestUtil by lazy { kosmos.shadeTestUtil }
- val userRepository by lazy { kosmos.fakeUserRepository }
- val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
- val dozeParameters by lazy { kosmos.dozeParameters }
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+ private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ private val powerRepository by lazy { kosmos.fakePowerRepository }
+ private val shadeRepository by lazy { kosmos.fakeShadeRepository }
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+ private val userRepository by lazy { kosmos.fakeUserRepository }
+ private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
+ private val dozeParameters by lazy { kosmos.dozeParameters }
lateinit var underTest: ShadeInteractorImpl
@@ -497,4 +497,24 @@
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
}
+
+ @Test
+ fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
+ }
+
+ @Test
+ fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 840aa92..26e1a4d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -26,6 +26,7 @@
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.Flags
import com.android.systemui.flags.andSceneContainer
@@ -36,6 +37,7 @@
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
@@ -51,6 +53,7 @@
import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -153,7 +156,7 @@
fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -196,7 +199,7 @@
fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -217,7 +220,7 @@
fun shouldShowEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -315,7 +318,7 @@
@Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -333,7 +336,7 @@
@Test
fun shouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -351,7 +354,7 @@
@Test
fun shouldIncludeFooterView_falseWhenKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -366,7 +369,7 @@
@Test
fun shouldIncludeFooterView_falseWhenUserNotSetUp() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -384,7 +387,7 @@
@Test
fun shouldIncludeFooterView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -402,7 +405,7 @@
@Test
fun shouldIncludeFooterView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -421,7 +424,7 @@
@Test
fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
- val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectFooterViewVisibility()
val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
@@ -444,7 +447,7 @@
@Test
fun shouldIncludeFooterView_falseWhenRemoteInputActive() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -462,7 +465,7 @@
@Test
fun shouldIncludeFooterView_animatesWhenShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -478,7 +481,7 @@
@Test
fun shouldIncludeFooterView_notAnimatingOnKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldInclude by collectFooterViewVisibility()
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -492,6 +495,22 @@
}
@Test
+ @EnableSceneContainer
+ fun shouldShowFooterView_falseWhenShadeIsClosed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+ // WHEN shade is closed
+ fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeTestUtil.setShadeExpansion(0f)
+ runCurrent()
+
+ // THEN footer is hidden
+ assertThat(shouldShow?.value).isFalse()
+ }
+
+ @Test
+ @DisableSceneContainer
fun shouldHideFooterView_trueWhenShadeIsClosed() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -506,6 +525,7 @@
}
@Test
+ @DisableSceneContainer
fun shouldHideFooterView_falseWhenShadeIsOpen() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -520,6 +540,7 @@
}
@Test
+ @DisableSceneContainer
fun shouldHideFooterView_falseWhenQSPartiallyOpen() =
testScope.runTest {
val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -642,4 +663,10 @@
assertThat(animationsEnabled).isTrue()
}
+
+ private fun TestScope.collectFooterViewVisibility() =
+ collectLastValue(
+ if (SceneContainerFlag.isEnabled) underTest.shouldShowFooterView
+ else underTest.shouldIncludeFooterView
+ )
}
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 141d035..e1808fa 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -2002,10 +2002,10 @@
<!-- Shadow for dream overlay status bar complications -->
<dimen name="dream_overlay_status_bar_key_text_shadow_dx">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_key_text_shadow_dy">0.5dp</dimen>
- <dimen name="dream_overlay_status_bar_key_text_shadow_radius">1dp</dimen>
+ <dimen name="dream_overlay_status_bar_key_text_shadow_radius">3dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
<dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
- <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+ <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">3dp</dimen>
<dimen name="dream_overlay_icon_inset_dimen">0dp</dimen>
<!-- Default device corner radius, used for assist UI -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index dd84bc6..92e5432 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -271,7 +271,8 @@
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
emergencyButtonController, mMessageAreaControllerFactory,
- mDevicePostureController, mFeatureFlags, mSelectedUserInteractor);
+ mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
+ mMSDLPlayer);
} else if (keyguardInputView instanceof KeyguardPasswordView) {
return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index caa74780..f74d93e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@
import com.android.internal.widget.LockscreenCredential;
import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
@@ -43,6 +44,8 @@
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
+import com.google.android.msdl.domain.MSDLPlayer;
+
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -67,6 +70,7 @@
private LockPatternView mLockPatternView;
private CountDownTimer mCountdownTimer;
private AsyncTask<?, ?, ?> mPendingLockCheck;
+ private MSDLPlayer mMSDLPlayer;
private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() {
@Override
@@ -75,6 +79,10 @@
}
};
+ private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> {
+ BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView);
+ };
+
/**
* Useful for clearing out the wrong pattern after a delay
*/
@@ -166,6 +174,10 @@
boolean isValidPattern) {
boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId;
if (matched) {
+ BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+ /* authenticationSucceeded= */true,
+ /* player =*/mMSDLPlayer
+ );
getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0);
if (dismissKeyguard) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
@@ -173,6 +185,10 @@
getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern);
}
} else {
+ BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+ /* authenticationSucceeded= */false,
+ /* player =*/mMSDLPlayer
+ );
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
if (isValidPattern) {
getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
@@ -200,7 +216,7 @@
EmergencyButtonController emergencyButtonController,
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
DevicePostureController postureController, FeatureFlags featureFlags,
- SelectedUserInteractor selectedUserInteractor) {
+ SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) {
super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
messageAreaControllerFactory, featureFlags, selectedUserInteractor);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -212,6 +228,7 @@
featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE));
mLockPatternView = mView.findViewById(R.id.lockPatternView);
mPostureController = postureController;
+ mMSDLPlayer = msdlPlayer;
}
@Override
@@ -249,6 +266,7 @@
if (deadline != 0) {
handleAttemptLockout(deadline);
}
+ mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer);
}
@Override
@@ -262,6 +280,7 @@
cancelBtn.setOnClickListener(null);
}
mPostureController.removeCallback(mPostureCallback);
+ mLockPatternView.setExternalHapticsPlayer(null);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
index 443441f..eb4de68 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
@@ -18,6 +18,9 @@
import static android.view.WindowManager.LayoutParams;
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,8 +32,8 @@
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.WindowManager;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.res.R;
/**
@@ -70,11 +73,12 @@
* @see #setDefaultPosition(LayoutParams)
*/
private final Point mControlPosition = new Point();
- private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mWindowManager;
MirrorWindowControl(Context context) {
mContext = context;
- mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+ enableViewCaptureTracing());
}
public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
new file mode 100644
index 0000000..4eb2274
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+data class CaptioningModel(
+ val isSystemAudioCaptioningUiEnabled: Boolean,
+ val isSystemAudioCaptioningEnabled: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
index bf749d4..5414b62 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
@@ -16,98 +16,90 @@
package com.android.systemui.accessibility.data.repository
+import android.annotation.SuppressLint
import android.view.accessibility.CaptioningManager
+import com.android.systemui.accessibility.data.model.CaptioningModel
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
interface CaptioningRepository {
- /** The system audio caption enabled state. */
- val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+ /** Current state of Live Captions. */
+ val captioningModel: StateFlow<CaptioningModel?>
- /** The system audio caption UI enabled state. */
- val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
-
- /** Sets [isSystemAudioCaptioningEnabled]. */
+ /** Sets [CaptioningModel.isSystemAudioCaptioningEnabled]. */
suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean)
}
-class CaptioningRepositoryImpl(
- private val captioningManager: CaptioningManager,
- private val backgroundCoroutineContext: CoroutineContext,
- coroutineScope: CoroutineScope,
+@OptIn(ExperimentalCoroutinesApi::class)
+class CaptioningRepositoryImpl
+@Inject
+constructor(
+ private val userScopedCaptioningManagerProvider: UserScopedService<CaptioningManager>,
+ userRepository: UserRepository,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Application coroutineScope: CoroutineScope,
) : CaptioningRepository {
- private val captioningChanges: SharedFlow<CaptioningChange> =
- callbackFlow {
- val listener = CaptioningChangeProducingListener(this)
- captioningManager.addCaptioningChangeListener(listener)
- awaitClose { captioningManager.removeCaptioningChangeListener(listener) }
- }
- .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
+ @SuppressLint("NonInjectedService") // this uses user-aware context
+ private val captioningManager: StateFlow<CaptioningManager?> =
+ userRepository.selectedUser
+ .map { userScopedCaptioningManagerProvider.forUser(it.userInfo.userHandle) }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
- override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> =
- captioningChanges
- .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class)
- .map { it.isEnabled }
- .onStart { emit(captioningManager.isSystemAudioCaptioningEnabled) }
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(),
- captioningManager.isSystemAudioCaptioningEnabled,
- )
-
- override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> =
- captioningChanges
- .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class)
- .map { it.isEnabled }
- .onStart { emit(captioningManager.isSystemAudioCaptioningUiEnabled) }
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(),
- captioningManager.isSystemAudioCaptioningUiEnabled,
- )
+ override val captioningModel: StateFlow<CaptioningModel?> =
+ captioningManager
+ .filterNotNull()
+ .flatMapLatest { it.captioningModel() }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
withContext(backgroundCoroutineContext) {
- captioningManager.isSystemAudioCaptioningEnabled = isEnabled
+ captioningManager.value?.isSystemAudioCaptioningEnabled = isEnabled
}
}
- private sealed interface CaptioningChange {
+ private fun CaptioningManager.captioningModel(): Flow<CaptioningModel> {
+ return conflatedCallbackFlow {
+ val listener =
+ object : CaptioningManager.CaptioningChangeListener() {
- data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
+ override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
+ trySend(Unit)
+ }
- data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
- }
-
- private class CaptioningChangeProducingListener(
- private val scope: ProducerScope<CaptioningChange>
- ) : CaptioningManager.CaptioningChangeListener() {
-
- override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
- emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled))
- }
-
- override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
- emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled))
- }
-
- private fun emitChange(change: CaptioningChange) {
- scope.launch { scope.send(change) }
- }
+ override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
+ trySend(Unit)
+ }
+ }
+ addCaptioningChangeListener(listener)
+ awaitClose { removeCaptioningChangeListener(listener) }
+ }
+ .onStart { emit(Unit) }
+ .map {
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled = isSystemAudioCaptioningEnabled,
+ isSystemAudioCaptioningUiEnabled = isSystemAudioCaptioningUiEnabled,
+ )
+ }
+ .flowOn(backgroundCoroutineContext)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
index 1d493c6..840edf4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
@@ -17,16 +17,22 @@
package com.android.systemui.accessibility.domain.interactor
import com.android.systemui.accessibility.data.repository.CaptioningRepository
-import kotlinx.coroutines.flow.StateFlow
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
-class CaptioningInteractor(private val repository: CaptioningRepository) {
+@SysUISingleton
+class CaptioningInteractor @Inject constructor(private val repository: CaptioningRepository) {
- val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
- get() = repository.isSystemAudioCaptioningEnabled
+ val isSystemAudioCaptioningEnabled: Flow<Boolean> =
+ repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningEnabled }
- val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
- get() = repository.isSystemAudioCaptioningUiEnabled
+ val isSystemAudioCaptioningUiEnabled: Flow<Boolean> =
+ repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningUiEnabled }
- suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) =
+ suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) {
repository.setIsSystemAudioCaptioningEnabled(enabled)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
new file mode 100644
index 0000000..1faacff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.helper
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
+//noinspection CleanArchitectureDependencyViolation: Data layer only referenced for this enum class
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+
+/** A helper object to deliver haptic feedback in bouncer interactions. */
+object BouncerHapticHelper {
+
+ private val authInteractionProperties = AuthInteractionProperties()
+
+ /**
+ * Deliver MSDL feedback as a result of authenticating through a bouncer.
+ *
+ * @param[authenticationSucceeded] Whether the authentication was successful or not.
+ * @param[player] The [MSDLPlayer] that delivers the correct feedback.
+ */
+ fun playMSDLAuthenticationFeedback(
+ authenticationSucceeded: Boolean,
+ player: MSDLPlayer?,
+ ) {
+ if (player == null || !Flags.msdlFeedback()) {
+ return
+ }
+
+ val token =
+ if (authenticationSucceeded) {
+ MSDLToken.UNLOCK
+ } else {
+ MSDLToken.FAILURE
+ }
+ player.playToken(token, authInteractionProperties)
+ }
+
+ /**
+ * Deliver feedback when dragging through cells in the pattern bouncer. This function can play
+ * MSDL feedback using a [MSDLPlayer], or fallback to a default haptic feedback using the
+ * [View.performHapticFeedback] API and a [View].
+ *
+ * @param[player] [MSDLPlayer] for MSDL feedback.
+ * @param[view] A [View] for default haptic feedback using [View.performHapticFeedback]
+ */
+ fun playPatternDotFeedback(player: MSDLPlayer?, view: View?) {
+ if (player == null || !Flags.msdlFeedback()) {
+ view?.performHapticFeedback(
+ HapticFeedbackConstants.VIRTUAL_KEY,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+ )
+ } else {
+ player.playToken(MSDLToken.DRAG_INDICATOR)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index c69cea4..04393fe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -21,6 +21,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
import com.android.systemui.Flags.restartDreamOnUnocclude
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -55,6 +56,7 @@
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val dreamManager: DreamManager,
+ private val communalSceneInteractor: CommunalSceneInteractor,
@Background private val bgScope: CoroutineScope,
) : CoreStartable {
/** Flow that emits when the dream should be started underneath the glanceable hub. */
@@ -66,6 +68,8 @@
not(keyguardInteractor.isDreaming),
// TODO(b/362830856): Remove this workaround.
keyguardInteractor.isKeyguardShowing,
+ not(communalSceneInteractor.isLaunchingWidget),
+ not(keyguardInteractor.isKeyguardOccluded),
)
.filter { it }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 21a704d..8818c3a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -202,6 +202,13 @@
return context.getSystemService(CaptioningManager.class);
}
+ @Provides
+ @Singleton
+ static UserScopedService<CaptioningManager> provideUserScopedCaptioningManager(
+ Context context) {
+ return new UserScopedServiceImpl<>(context, CaptioningManager.class);
+ }
+
/** */
@Provides
@Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 1bc91ca..67625d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -81,6 +81,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardStateCallbackInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardWakeDirectlyToGoneInteractor;
import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier;
import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder;
@@ -126,6 +127,7 @@
private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
private final Executor mMainExecutor;
private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy;
+ private final KeyguardStateCallbackInteractor mKeyguardStateCallbackInteractor;
private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -350,7 +352,8 @@
Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy,
KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor,
KeyguardDismissInteractor keyguardDismissInteractor,
- Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy) {
+ Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
+ KeyguardStateCallbackInteractor keyguardStateCallbackInteractor) {
super();
mKeyguardViewMediator = keyguardViewMediator;
mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -363,6 +366,7 @@
mSceneInteractorLazy = sceneInteractorLazy;
mMainExecutor = mainExecutor;
mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy;
+ mKeyguardStateCallbackInteractor = keyguardStateCallbackInteractor;
mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
if (KeyguardWmStateRefactor.isEnabled()) {
@@ -455,6 +459,8 @@
checkPermission();
if (SceneContainerFlag.isEnabled()) {
mKeyguardStateCallbackStartableLazy.get().addCallback(callback);
+ } else if (KeyguardWmStateRefactor.isEnabled()) {
+ mKeyguardStateCallbackInteractor.addCallback(callback);
} else {
mKeyguardViewMediator.addStateMonitorCallback(callback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d38c952..3b1569d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2288,6 +2288,10 @@
}
private void updateInputRestrictedLocked() {
+ if (KeyguardWmStateRefactor.isEnabled()) {
+ return;
+ }
+
boolean inputRestricted = isInputRestricted();
if (mInputRestricted != inputRestricted) {
mInputRestricted = inputRestricted;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index db5a63b..58c8a04 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -73,7 +73,7 @@
if (SceneContainerFlag.isEnabled) return
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
- listenForGoneToLockscreenOrHub()
+ listenForGoneToLockscreenOrHubOrOccluded()
listenForGoneToOccluded()
listenForGoneToDreamingLockscreenHosted()
}
@@ -89,22 +89,19 @@
*/
private fun listenForGoneToOccluded() {
scope.launch("$TAG#listenForGoneToOccluded") {
- keyguardInteractor.showDismissibleKeyguard
- .filterRelevantKeyguardState()
- .sample(keyguardInteractor.isKeyguardOccluded, ::Pair)
- .collect { (_, isKeyguardOccluded) ->
- if (isKeyguardOccluded) {
- startTransitionTo(
- KeyguardState.OCCLUDED,
- ownerReason = "Dismissible keyguard with occlusion"
- )
- }
+ keyguardInteractor.showDismissibleKeyguard.filterRelevantKeyguardState().collect {
+ if (keyguardInteractor.isKeyguardOccluded.value) {
+ startTransitionTo(
+ KeyguardState.OCCLUDED,
+ ownerReason = "Dismissible keyguard with occlusion"
+ )
}
+ }
}
}
// Primarily for when the user chooses to lock down the device
- private fun listenForGoneToLockscreenOrHub() {
+ private fun listenForGoneToLockscreenOrHubOrOccluded() {
if (KeyguardWmStateRefactor.isEnabled) {
scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
biometricSettingsRepository.isCurrentUserInLockdown
@@ -137,7 +134,7 @@
}
}
} else {
- scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
+ scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") {
keyguardInteractor.isKeyguardShowing
.filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
.sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
@@ -145,6 +142,8 @@
val to =
if (isIdleOnCommunal) {
KeyguardState.GLANCEABLE_HUB
+ } else if (keyguardInteractor.isKeyguardOccluded.value) {
+ KeyguardState.OCCLUDED
} else {
KeyguardState.LOCKSCREEN
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt
new file mode 100644
index 0000000..420fbd4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.DeadObjectException
+import android.os.RemoteException
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * Updates KeyguardStateCallbacks provided to KeyguardService with KeyguardTransitionInteractor
+ * state.
+ *
+ * This borrows heavily from [KeyguardStateCallbackStartable], which requires Flexiglass. This class
+ * can be removed after Flexiglass launches.
+ */
+@SysUISingleton
+class KeyguardStateCallbackInteractor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val selectedUserInteractor: SelectedUserInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : CoreStartable {
+ private val callbacks = mutableListOf<IKeyguardStateCallback>()
+
+ override fun start() {
+ if (!KeyguardWmStateRefactor.isEnabled || SceneContainerFlag.isEnabled) {
+ return
+ }
+
+ applicationScope.launch {
+ combine(
+ selectedUserInteractor.selectedUser,
+ keyguardTransitionInteractor.currentKeyguardState,
+ ::Pair
+ ).collectLatest { (selectedUser, currentState) ->
+ val iterator = callbacks.iterator()
+ withContext(backgroundDispatcher) {
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ try {
+ callback.onShowingStateChanged(
+ currentState != KeyguardState.GONE,
+ selectedUser
+ )
+ callback.onInputRestrictedStateChanged(
+ currentState != KeyguardState.GONE)
+ } catch (e: RemoteException) {
+ if (e is DeadObjectException) {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun addCallback(callback: IKeyguardStateCallback) {
+ KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
+ callbacks.add(callback)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index d9c48fa..25b8fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -29,6 +29,7 @@
private val auditLogger: KeyguardTransitionAuditLogger,
private val bootInteractor: KeyguardTransitionBootInteractor,
private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor,
+ private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor,
) : CoreStartable {
override fun start() {
@@ -55,6 +56,7 @@
auditLogger.start()
bootInteractor.start()
statusBarDisableFlagsInteractor.start()
+ keyguardStateCallbackInteractor.start()
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 24c57be..4ad437c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -76,6 +76,7 @@
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.ui.view.MediaViewHolder
@@ -943,7 +944,7 @@
desc.subtitle,
desc.title,
artworkIcon,
- listOf(mediaAction),
+ listOf(),
listOf(0),
MediaButton(playOrPause = mediaAction),
packageName,
@@ -1074,13 +1075,13 @@
}
// Control buttons
- // If flag is enabled and controller has a PlaybackState, create actions from session info
+ // If controller has a PlaybackState, create actions from session info
// Otherwise, use the notification actions
- var actionIcons: List<MediaAction> = emptyList()
+ var actionIcons: List<MediaNotificationAction> = emptyList()
var actionsToShowCollapsed: List<Int> = emptyList()
val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
if (semanticActions == null) {
- val actions = createActionsFromNotification(context, activityStarter, sbn)
+ val actions = createActionsFromNotification(context, sbn)
actionIcons = actions.first
actionsToShowCollapsed = actions.second
}
@@ -1464,7 +1465,7 @@
val updated =
data.copy(
token = null,
- actions = actions,
+ actions = listOf(),
semanticActions = MediaButton(playOrPause = resumeAction),
actionsToShowInCompact = listOf(0),
active = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index bcf748e..f2825d0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -33,6 +33,7 @@
import com.android.systemui.media.controls.shared.MediaControlDrawables
import com.android.systemui.media.controls.shared.model.MediaAction
import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
@@ -217,11 +218,10 @@
/** Generate action buttons based on notification actions */
fun createActionsFromNotification(
context: Context,
- activityStarter: ActivityStarter,
sbn: StatusBarNotification
-): Pair<List<MediaAction>, List<Int>> {
+): Pair<List<MediaNotificationAction>, List<Int>> {
val notif = sbn.notification
- val actionIcons: MutableList<MediaAction> = ArrayList()
+ val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
val actions = notif.actions
var actionsToShowCollapsed =
notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
@@ -250,25 +250,6 @@
continue
}
- val runnable =
- action.actionIntent?.let { actionIntent ->
- Runnable {
- when {
- actionIntent.isActivity ->
- activityStarter.startPendingIntentDismissingKeyguard(
- action.actionIntent
- )
- action.isAuthenticationRequired ->
- activityStarter.dismissKeyguardThenExecute(
- { sendPendingIntent(action.actionIntent) },
- {},
- true
- )
- else -> sendPendingIntent(actionIntent)
- }
- }
- }
-
val themeText =
com.android.settingslib.Utils.getColorAttr(
context,
@@ -285,13 +266,53 @@
.setTint(themeText)
.loadDrawable(context)
- val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+ val mediaAction =
+ MediaNotificationAction(
+ action.isAuthenticationRequired,
+ action.actionIntent,
+ mediaActionIcon,
+ action.title
+ )
actionIcons.add(mediaAction)
}
}
return Pair(actionIcons, actionsToShowCollapsed)
}
+/**
+ * Converts [MediaNotificationAction] list into [MediaAction] list
+ *
+ * @param actions list of [MediaNotificationAction]
+ * @param activityStarter starter for activities
+ * @return list of [MediaAction]
+ */
+fun getNotificationActions(
+ actions: List<MediaNotificationAction>,
+ activityStarter: ActivityStarter
+): List<MediaAction> {
+ return actions.map { action ->
+ val runnable =
+ action.actionIntent?.let { actionIntent ->
+ Runnable {
+ when {
+ actionIntent.isActivity ->
+ activityStarter.startPendingIntentDismissingKeyguard(
+ action.actionIntent
+ )
+ action.isAuthenticationRequired ->
+ activityStarter.dismissKeyguardThenExecute(
+ { sendPendingIntent(action.actionIntent) },
+ {},
+ true
+ )
+ else -> sendPendingIntent(actionIntent)
+ }
+ }
+ }
+ MediaAction(action.icon, runnable, action.contentDescription, background = null)
+ }
+}
+
private fun sendPendingIntent(intent: PendingIntent): Boolean {
return try {
intent.send(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index f9fef8e..53cc15b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -54,10 +54,10 @@
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
@@ -80,7 +80,6 @@
@Application val context: Context,
@Main val mainDispatcher: CoroutineDispatcher,
@Background val backgroundScope: CoroutineScope,
- private val activityStarter: ActivityStarter,
private val mediaControllerFactory: MediaControllerFactory,
private val mediaFlags: MediaFlags,
private val imageLoader: ImageLoader,
@@ -209,15 +208,14 @@
val device: MediaDeviceData? = getDeviceInfoForRemoteCast(key, sbn)
// Control buttons
- // If flag is enabled and controller has a PlaybackState, create actions from session
- // info
+ // If controller has a PlaybackState, create actions from session info
// Otherwise, use the notification actions
- var actionIcons: List<MediaAction> = emptyList()
+ var actionIcons: List<MediaNotificationAction> = emptyList()
var actionsToShowCollapsed: List<Int> = emptyList()
val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
logD(TAG) { "Semantic actions: $semanticActions" }
if (semanticActions == null) {
- val actions = createActionsFromNotification(context, activityStarter, sbn)
+ val actions = createActionsFromNotification(context, sbn)
actionIcons = actions.first
actionsToShowCollapsed = actions.second
logD(TAG) { "[!!] Semantic actions: $semanticActions" }
@@ -329,7 +327,7 @@
artist = desc.subtitle,
song = desc.title,
artworkIcon = artworkIcon,
- actionIcons = listOf(mediaAction),
+ actionIcons = listOf(),
actionsToShowInCompact = listOf(0),
semanticActions = MediaButton(playOrPause = mediaAction),
token = token,
@@ -514,7 +512,7 @@
val artist: CharSequence?,
val song: CharSequence?,
val artworkIcon: Icon?,
- val actionIcons: List<MediaAction>,
+ val actionIcons: List<MediaNotificationAction>,
val actionsToShowInCompact: List<Int>,
val semanticActions: MediaButton?,
val token: MediaSession.Token?,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 4555810..5f0a9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -71,12 +71,14 @@
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.MediaLogger
import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.shared.model.MediaAction
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.ui.view.MediaViewHolder
@@ -149,6 +151,7 @@
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val mediaDataRepository: MediaDataRepository,
private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+ private val mediaLogger: MediaLogger,
) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
companion object {
@@ -228,6 +231,7 @@
keyguardUpdateMonitor: KeyguardUpdateMonitor,
mediaDataRepository: MediaDataRepository,
mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+ mediaLogger: MediaLogger,
) : this(
context,
applicationScope,
@@ -253,6 +257,7 @@
keyguardUpdateMonitor,
mediaDataRepository,
mediaDataLoader,
+ mediaLogger,
)
private val appChangeReceiver =
@@ -794,7 +799,7 @@
desc.subtitle,
desc.title,
artworkIcon,
- listOf(mediaAction),
+ listOf(),
listOf(0),
MediaButton(playOrPause = mediaAction),
packageName,
@@ -832,12 +837,48 @@
return@withContext
}
- val currentEntry = mediaDataRepository.mediaEntries.value[key]
- val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
- val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
- val resumeAction: Runnable? = currentEntry?.resumeAction
- val hasCheckedForResume = currentEntry?.hasCheckedForResume == true
- val active = currentEntry?.active ?: true
+ val mediaController = mediaControllerFactory.create(result.token!!)
+ val oldEntry = mediaDataRepository.mediaEntries.value[key]
+ val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId()
+ val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L
+ val resumeAction: Runnable? = oldEntry?.resumeAction
+ val hasCheckedForResume = oldEntry?.hasCheckedForResume == true
+ val active = oldEntry?.active ?: true
+
+ val mediaData =
+ MediaData(
+ userId = sbn.normalizedUserId,
+ initialized = true,
+ app = result.appName,
+ appIcon = result.appIcon,
+ artist = result.artist,
+ song = result.song,
+ artwork = result.artworkIcon,
+ actions = result.actionIcons,
+ actionsToShowInCompact = result.actionsToShowInCompact,
+ semanticActions = result.semanticActions,
+ packageName = sbn.packageName,
+ token = result.token,
+ clickIntent = result.clickIntent,
+ device = result.device,
+ active = active,
+ resumeAction = resumeAction,
+ playbackLocation = result.playbackLocation,
+ notificationKey = key,
+ hasCheckedForResume = hasCheckedForResume,
+ isPlaying = result.isPlaying,
+ isClearable = !sbn.isOngoing,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = result.appUid,
+ isExplicit = result.isExplicit,
+ )
+
+ if (isSameMediaData(context, mediaController, mediaData, oldEntry)) {
+ mediaLogger.logDuplicateMediaNotification(key)
+ return@withContext
+ }
// We need to log the correct media added.
if (isNewlyActiveEntry) {
@@ -848,7 +889,7 @@
instanceId,
result.playbackLocation
)
- } else if (result.playbackLocation != currentEntry?.playbackLocation) {
+ } else if (result.playbackLocation != oldEntry?.playbackLocation) {
logger.logPlaybackLocationChange(
result.appUid,
sbn.packageName,
@@ -857,40 +898,7 @@
)
}
- withContext(mainDispatcher) {
- onMediaDataLoaded(
- key,
- oldKey,
- MediaData(
- userId = sbn.normalizedUserId,
- initialized = true,
- app = result.appName,
- appIcon = result.appIcon,
- artist = result.artist,
- song = result.song,
- artwork = result.artworkIcon,
- actions = result.actionIcons,
- actionsToShowInCompact = result.actionsToShowInCompact,
- semanticActions = result.semanticActions,
- packageName = sbn.packageName,
- token = result.token,
- clickIntent = result.clickIntent,
- device = result.device,
- active = active,
- resumeAction = resumeAction,
- playbackLocation = result.playbackLocation,
- notificationKey = key,
- hasCheckedForResume = hasCheckedForResume,
- isPlaying = result.isPlaying,
- isClearable = !sbn.isOngoing,
- lastActive = lastActive,
- createdTimestampMillis = createdTimestampMillis,
- instanceId = instanceId,
- appUid = result.appUid,
- isExplicit = result.isExplicit,
- )
- )
- }
+ withContext(mainDispatcher) { onMediaDataLoaded(key, oldKey, mediaData) }
}
@Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up")
@@ -1001,13 +1009,13 @@
}
// Control buttons
- // If flag is enabled and controller has a PlaybackState, create actions from session info
+ // If controller has a PlaybackState, create actions from session info
// Otherwise, use the notification actions
- var actionIcons: List<MediaAction> = emptyList()
+ var actionIcons: List<MediaNotificationAction> = emptyList()
var actionsToShowCollapsed: List<Int> = emptyList()
val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
if (semanticActions == null) {
- val actions = createActionsFromNotification(context, activityStarter, sbn)
+ val actions = createActionsFromNotification(context, sbn)
actionIcons = actions.first
actionsToShowCollapsed = actions.second
}
@@ -1022,57 +1030,72 @@
else MediaData.PLAYBACK_CAST_LOCAL
val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) }
- val currentEntry = mediaDataRepository.mediaEntries.value.get(key)
- val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+ val oldEntry = mediaDataRepository.mediaEntries.value.get(key)
+ val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId()
val appUid = appInfo?.uid ?: Process.INVALID_UID
+ val lastActive = systemClock.elapsedRealtime()
+ val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L
+ val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
+ val hasCheckedForResume =
+ mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
+ val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+ var mediaData =
+ MediaData(
+ sbn.normalizedUserId,
+ true,
+ appName,
+ smallIcon,
+ artist,
+ song,
+ artWorkIcon,
+ actionIcons,
+ actionsToShowCollapsed,
+ semanticActions,
+ sbn.packageName,
+ token,
+ notif.contentIntent,
+ device,
+ active,
+ resumeAction = resumeAction,
+ playbackLocation = playbackLocation,
+ notificationKey = key,
+ hasCheckedForResume = hasCheckedForResume,
+ isPlaying = isPlaying,
+ isClearable = !sbn.isOngoing,
+ lastActive = lastActive,
+ createdTimestampMillis = createdTimestampMillis,
+ instanceId = instanceId,
+ appUid = appUid,
+ isExplicit = isExplicit,
+ smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
+ )
+
+ if (isSameMediaData(context, mediaController, mediaData, oldEntry)) {
+ mediaLogger.logDuplicateMediaNotification(key)
+ return
+ }
+
if (isNewlyActiveEntry) {
logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
- } else if (playbackLocation != currentEntry?.playbackLocation) {
+ } else if (playbackLocation != oldEntry?.playbackLocation) {
logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
}
- val lastActive = systemClock.elapsedRealtime()
- val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
foregroundExecutor.execute {
- val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
- val hasCheckedForResume =
+ val oldResumeAction: Runnable? =
+ mediaDataRepository.mediaEntries.value[key]?.resumeAction
+ val oldHasCheckedForResume =
mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
- val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
- onMediaDataLoaded(
- key,
- oldKey,
- MediaData(
- sbn.normalizedUserId,
- true,
- appName,
- smallIcon,
- artist,
- song,
- artWorkIcon,
- actionIcons,
- actionsToShowCollapsed,
- semanticActions,
- sbn.packageName,
- token,
- notif.contentIntent,
- device,
- active,
- resumeAction = resumeAction,
- playbackLocation = playbackLocation,
- notificationKey = key,
- hasCheckedForResume = hasCheckedForResume,
- isPlaying = isPlaying,
- isClearable = !sbn.isOngoing,
- lastActive = lastActive,
- createdTimestampMillis = createdTimestampMillis,
- instanceId = instanceId,
- appUid = appUid,
- isExplicit = isExplicit,
- smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
+ val oldActive = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+ mediaData =
+ mediaData.copy(
+ resumeAction = oldResumeAction,
+ hasCheckedForResume = oldHasCheckedForResume,
+ active = oldActive
)
- )
+ onMediaDataLoaded(key, oldKey, mediaData)
}
}
@@ -1402,7 +1425,7 @@
val updated =
data.copy(
token = null,
- actions = actions,
+ actions = listOf(),
semanticActions = MediaButton(playOrPause = resumeAction),
actionsToShowInCompact = listOf(0),
active = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
new file mode 100644
index 0000000..55d7b1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.annotation.WorkerThread
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.util.Log
+import com.android.systemui.Flags.mediaControlsPostsOptimization
+import com.android.systemui.biometrics.Utils.toBitmap
+import com.android.systemui.media.controls.shared.model.MediaData
+
+private const val TAG = "MediaProcessingHelper"
+
+/**
+ * Compares [new] media data to [old] media data.
+ *
+ * @param context Context
+ * @param newController media controller of the new media data.
+ * @param new new media data.
+ * @param old old media data.
+ * @return whether new and old contain same data
+ */
+fun isSameMediaData(
+ context: Context,
+ newController: MediaController,
+ new: MediaData,
+ old: MediaData?
+): Boolean {
+ if (old == null || !mediaControlsPostsOptimization()) return false
+
+ return new.userId == old.userId &&
+ new.app == old.app &&
+ new.artist == old.artist &&
+ new.song == old.song &&
+ new.packageName == old.packageName &&
+ new.isExplicit == old.isExplicit &&
+ new.appUid == old.appUid &&
+ new.notificationKey == old.notificationKey &&
+ new.isPlaying == old.isPlaying &&
+ new.isClearable == old.isClearable &&
+ new.playbackLocation == old.playbackLocation &&
+ new.device == old.device &&
+ new.initialized == old.initialized &&
+ new.resumption == old.resumption &&
+ new.token == old.token &&
+ new.resumeProgress == old.resumeProgress &&
+ areClickIntentsEqual(new.clickIntent, old.clickIntent) &&
+ areActionsEqual(context, newController, new, old) &&
+ areIconsEqual(context, new.artwork, old.artwork) &&
+ areIconsEqual(context, new.appIcon, old.appIcon)
+}
+
+/** Returns whether actions lists are equal. */
+fun areCustomActionListsEqual(
+ first: List<PlaybackState.CustomAction>?,
+ second: List<PlaybackState.CustomAction>?
+): Boolean {
+ // Same object, or both null
+ if (first === second) {
+ return true
+ }
+
+ // Only one null, or different number of actions
+ if ((first == null || second == null) || (first.size != second.size)) {
+ return false
+ }
+
+ // Compare individual actions
+ first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
+ if (!areCustomActionsEqual(firstAction, secondAction)) {
+ return false
+ }
+ }
+ return true
+}
+
+private fun areCustomActionsEqual(
+ firstAction: PlaybackState.CustomAction,
+ secondAction: PlaybackState.CustomAction
+): Boolean {
+ if (
+ firstAction.action != secondAction.action ||
+ firstAction.name != secondAction.name ||
+ firstAction.icon != secondAction.icon
+ ) {
+ return false
+ }
+
+ if ((firstAction.extras == null) != (secondAction.extras == null)) {
+ return false
+ }
+ if (firstAction.extras != null) {
+ firstAction.extras.keySet().forEach { key ->
+ if (firstAction.extras[key] != secondAction.extras[key]) {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+@WorkerThread
+private fun areIconsEqual(context: Context, new: Icon?, old: Icon?): Boolean {
+ if (new == old) return true
+ if (new == null || old == null || new.type != old.type) return false
+ return if (new.type == Icon.TYPE_BITMAP || new.type == Icon.TYPE_ADAPTIVE_BITMAP) {
+ if (new.bitmap.isRecycled || old.bitmap.isRecycled) {
+ Log.e(TAG, "Cannot compare recycled bitmap")
+ return false
+ }
+ new.bitmap.sameAs(old.bitmap)
+ } else {
+ val newDrawable = new.loadDrawable(context)
+ val oldDrawable = old.loadDrawable(context)
+
+ return newDrawable?.toBitmap()?.sameAs(oldDrawable?.toBitmap()) ?: false
+ }
+}
+
+private fun areActionsEqual(
+ context: Context,
+ newController: MediaController,
+ new: MediaData,
+ old: MediaData
+): Boolean {
+ val oldState = MediaController(context, old.token!!).playbackState
+ return if (
+ new.semanticActions == null &&
+ old.semanticActions == null &&
+ new.actions.size == old.actions.size
+ ) {
+ var same = true
+ new.actions.asSequence().zip(old.actions.asSequence()).forEach {
+ if (
+ it.first.actionIntent?.intent?.filterEquals(it.second.actionIntent?.intent) !=
+ true ||
+ it.first.icon != it.second.icon ||
+ it.first.contentDescription != it.second.contentDescription
+ ) {
+ same = false
+ return@forEach
+ }
+ }
+ same
+ } else if (new.semanticActions != null && old.semanticActions != null) {
+ oldState?.actions == newController.playbackState?.actions &&
+ areCustomActionListsEqual(
+ oldState?.customActions,
+ newController.playbackState?.customActions
+ )
+ } else {
+ false
+ }
+}
+
+private fun areClickIntentsEqual(newIntent: PendingIntent?, oldIntent: PendingIntent?): Boolean {
+ if ((newIntent == null && oldIntent == null) || newIntent === oldIntent) return true
+ if (newIntent == null || oldIntent == null) return false
+
+ return newIntent.intent?.filterEquals(oldIntent.intent) == true
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index fc31903..275f1ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -16,12 +16,14 @@
package com.android.systemui.media.controls.domain.pipeline
+import android.annotation.WorkerThread
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.SystemProperties
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -32,6 +34,7 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
+import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -49,6 +52,8 @@
@Inject
constructor(
private val mediaControllerFactory: MediaControllerFactory,
+ @Background private val bgExecutor: Executor,
+ @Main private val uiExecutor: Executor,
@Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController,
@@ -147,19 +152,21 @@
}
reusedListener?.let {
- val wasPlaying = it.isPlaying()
- logger.logUpdateListener(key, wasPlaying)
- it.setMediaData(data)
- it.key = key
- mediaListeners[key] = it
- if (wasPlaying != it.isPlaying()) {
- // If a player becomes active because of a migration, we'll need to broadcast
- // its state. Doing it now would lead to reentrant callbacks, so let's wait
- // until we're done.
- mainExecutor.execute {
- if (mediaListeners[key]?.isPlaying() == true) {
- logger.logDelayedUpdate(key)
- timeoutCallback.invoke(key, false /* timedOut */)
+ bgExecutor.execute {
+ val wasPlaying = it.isPlaying()
+ logger.logUpdateListener(key, wasPlaying)
+ it.setMediaData(data)
+ it.key = key
+ mediaListeners[key] = it
+ if (wasPlaying != it.isPlaying()) {
+ // If a player becomes active because of a migration, we'll need to broadcast
+ // its state. Doing it now would lead to reentrant callbacks, so let's wait
+ // until we're done.
+ mainExecutor.execute {
+ if (mediaListeners[key]?.isPlaying() == true) {
+ logger.logDelayedUpdate(key)
+ timeoutCallback.invoke(key, false /* timedOut */)
+ }
}
}
}
@@ -217,18 +224,20 @@
private set
fun Int.isPlaying() = isPlayingState(this)
+
fun isPlaying() = lastState?.state?.isPlaying() ?: false
init {
- setMediaData(data)
+ bgExecutor.execute { setMediaData(data) }
}
fun destroy() {
- mediaController?.unregisterCallback(this)
+ bgExecutor.execute { mediaController?.unregisterCallback(this) }
cancellation?.run()
destroyed = true
}
+ @WorkerThread
fun setMediaData(data: MediaData) {
sessionToken = data.token
destroyed = false
@@ -258,7 +267,7 @@
if (resumption == true) {
// Some apps create a session when MBS is queried. We should unregister the
// controller since it will no longer be valid, but don't cancel the timeout
- mediaController?.unregisterCallback(this)
+ bgExecutor.execute { mediaController?.unregisterCallback(this) }
} else {
// For active controls, if the session is destroyed, clean up everything since we
// will need to recreate it if this key is updated later
@@ -284,7 +293,7 @@
if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) {
logger.logStateCallback(key)
- stateCallback.invoke(key, state)
+ uiExecutor.execute { stateCallback.invoke(key, state) }
}
if (playingStateSame && !resumptionChanged) {
@@ -313,7 +322,7 @@
expireMediaTimeout(key, "playback started - $state, $key")
timedOut = false
if (dispatchEvents) {
- timeoutCallback(key, timedOut)
+ uiExecutor.execute { timeoutCallback(key, timedOut) }
}
}
}
@@ -337,60 +346,13 @@
}
}
- private fun areCustomActionListsEqual(
- first: List<PlaybackState.CustomAction>?,
- second: List<PlaybackState.CustomAction>?
- ): Boolean {
- // Same object, or both null
- if (first === second) {
- return true
- }
-
- // Only one null, or different number of actions
- if ((first == null || second == null) || (first.size != second.size)) {
- return false
- }
-
- // Compare individual actions
- first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
- if (!areCustomActionsEqual(firstAction, secondAction)) {
- return false
- }
- }
- return true
- }
-
- private fun areCustomActionsEqual(
- firstAction: PlaybackState.CustomAction,
- secondAction: PlaybackState.CustomAction
- ): Boolean {
- if (
- firstAction.action != secondAction.action ||
- firstAction.name != secondAction.name ||
- firstAction.icon != secondAction.icon
- ) {
- return false
- }
-
- if ((firstAction.extras == null) != (secondAction.extras == null)) {
- return false
- }
- if (firstAction.extras != null) {
- firstAction.extras.keySet().forEach { key ->
- if (firstAction.extras.get(key) != secondAction.extras.get(key)) {
- return false
- }
- }
- }
- return true
- }
-
/** Listens to changes in recommendation card data and schedules a timeout for its expiration */
private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
private var timedOut = false
var destroyed = false
var expiration = Long.MAX_VALUE
private set
+
var cancellation: Runnable? = null
private set
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 245f6f8..130868d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -32,6 +32,7 @@
import com.android.systemui.bluetooth.BroadcastDialogController
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.getNotificationActions
import com.android.systemui.media.controls.shared.model.MediaControlModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.util.MediaSmartspaceLogger
@@ -102,7 +103,7 @@
artwork = artwork,
deviceData = device,
semanticActionButtons = semanticActions,
- notificationActionButtons = actions,
+ notificationActionButtons = getNotificationActions(data.actions, activityStarter),
actionsToShowInCollapsed = actionsToShowInCompact,
isDismissible = isClearable,
isResume = resumption,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
index 2b710b5..7d20e17 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
@@ -114,6 +114,15 @@
)
}
+ fun logDuplicateMediaNotification(key: String) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = key },
+ { "duplicate media notification $str1 posted" }
+ )
+ }
+
companion object {
private const val TAG = "MediaLog"
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
index 40b3477..aed8609 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
@@ -39,7 +39,7 @@
/** Album artwork. */
val artwork: Icon? = null,
/** List of generic action buttons for the media player, based on notification actions */
- val actions: List<MediaAction> = emptyList(),
+ val actions: List<MediaNotificationAction> = emptyList(),
/** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
val actionsToShowInCompact: List<Int> = emptyList(),
/**
@@ -162,6 +162,14 @@
val rebindId: Int? = null
)
+/** State of a media action from notification. */
+data class MediaNotificationAction(
+ val isAuthenticationRequired: Boolean,
+ val actionIntent: PendingIntent?,
+ val icon: Drawable?,
+ val contentDescription: CharSequence?
+)
+
/** State of the media device. */
data class MediaDeviceData
@JvmOverloads
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 87610cf..8bec46a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -21,6 +21,7 @@
import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
import static com.android.systemui.Flags.communalHub;
import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation;
+import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions;
import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
import android.animation.Animator;
@@ -1170,7 +1171,7 @@
// Set all the generic buttons
List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
- List<MediaAction> actions = data.getActions();
+ List<MediaAction> actions = getNotificationActions(data.getActions(), mActivityStarter);
int i = 0;
for (; i < actions.size() && i < genericButtons.size(); i++) {
boolean showInCompact = actionsWhenCollapsed.contains(i);
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
index a5c07bc..11854d9 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -34,8 +38,10 @@
override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
setActions(
mapOf(
- Swipe.Up to SceneFamilies.Home,
Back to SceneFamilies.Home,
+ Swipe.Up to SceneFamilies.Home,
+ Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+ ReplaceByOverlay(Overlays.QuickSettingsShade),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index cbcf68c..2f843ac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -50,10 +50,12 @@
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.SystemClock;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
@@ -95,6 +97,7 @@
// Bind retry control.
private static final int MAX_BIND_RETRIES = 5;
private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS;
+ private static final long ACTIVE_TILE_BIND_RETRY_DELAY = 1 * DateUtils.SECOND_IN_MILLIS;
private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS;
private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000;
private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION =
@@ -107,6 +110,7 @@
private final Intent mIntent;
private final UserHandle mUser;
private final DelayableExecutor mExecutor;
+ private final SystemClock mSystemClock;
private final IBinder mToken = new Binder();
private final PackageManagerAdapter mPackageManagerAdapter;
private final BroadcastDispatcher mBroadcastDispatcher;
@@ -120,7 +124,6 @@
private IBinder mClickBinder;
private int mBindTryCount;
- private long mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
private AtomicBoolean isDeathRebindScheduled = new AtomicBoolean(false);
private AtomicBoolean mBound = new AtomicBoolean(false);
private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false);
@@ -138,7 +141,8 @@
TileLifecycleManager(@Main Handler handler, Context context, IQSService service,
PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher,
@Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager,
- IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) {
+ IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor,
+ SystemClock systemClock) {
mContext = context;
mHandler = handler;
mIntent = intent;
@@ -146,6 +150,7 @@
mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
mUser = user;
mExecutor = executor;
+ mSystemClock = systemClock;
mPackageManagerAdapter = packageManagerAdapter;
mBroadcastDispatcher = broadcastDispatcher;
mActivityManager = activityManager;
@@ -436,25 +441,31 @@
// If mBound is true (meaning that we should be bound), then reschedule binding for
// later.
if (mBound.get() && checkComponentState()) {
- if (isDeathRebindScheduled.compareAndSet(false, true)) {
+ if (isDeathRebindScheduled.compareAndSet(false, true)) { // if already not scheduled
+
+
mExecutor.executeDelayed(() -> {
// Only rebind if we are supposed to, but remove the scheduling anyway.
if (mBound.get()) {
setBindService(true);
}
- isDeathRebindScheduled.set(false);
+ isDeathRebindScheduled.set(false); // allow scheduling again
}, getRebindDelay());
}
}
});
}
+ private long mLastRebind = 0;
/**
* @return the delay to automatically rebind after a service died. It provides a longer delay if
* the device is a low memory state because the service is likely to get killed again by the
* system. In this case we want to rebind later and not to cause a loop of a frequent rebinds.
+ * It also provides a longer delay if called quickly (a few seconds) after a first call.
*/
private long getRebindDelay() {
+ final long now = mSystemClock.currentTimeMillis();
+
final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
mActivityManager.getMemoryInfo(info);
@@ -462,7 +473,20 @@
if (info.lowMemory) {
delay = LOW_MEMORY_BIND_RETRY_DELAY;
} else {
- delay = mBindRetryDelay;
+ if (Flags.qsQuickRebindActiveTiles()) {
+ final long elapsedTimeSinceLastRebind = now - mLastRebind;
+ final boolean justAttemptedRebind =
+ elapsedTimeSinceLastRebind < DEFAULT_BIND_RETRY_DELAY;
+ if (isActiveTile() && !justAttemptedRebind) {
+ delay = ACTIVE_TILE_BIND_RETRY_DELAY;
+ } else {
+ delay = DEFAULT_BIND_RETRY_DELAY;
+ }
+ } else {
+ delay = DEFAULT_BIND_RETRY_DELAY;
+ }
+
+ mLastRebind = now;
}
if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent());
return delay;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index d10471d..c5fa8cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -44,7 +44,7 @@
/**
* Manages the priority which lets {@link TileServices} make decisions about which tiles
* to bind. Also holds on to and manages the {@link TileLifecycleManager}, informing it
- * of when it is allowed to bind based on decisions frome the {@link TileServices}.
+ * of when it is allowed to bind based on decisions from the {@link TileServices}.
*/
public class TileServiceManager {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
index d3dc302..bd1872d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -43,6 +47,13 @@
.map { editing ->
buildMap {
put(Swipe.Up, UserActionResult(SceneFamilies.Home))
+ put(
+ Swipe(
+ direction = SwipeDirection.Down,
+ fromSource = SceneContainerEdge.TopLeft
+ ),
+ ReplaceByOverlay(Overlays.NotificationsShade)
+ )
if (!editing) {
put(Back, UserActionResult(SceneFamilies.Home))
}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 3d6d00e..a5f4a89 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -23,8 +23,6 @@
import android.content.res.Resources
import android.net.Uri
import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
import android.util.Log
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogTransitionAnimator
@@ -50,11 +48,11 @@
notificationManager: NotificationManager,
userContextProvider: UserContextProvider,
keyguardDismissUtil: KeyguardDismissUtil,
- private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val panelInteractor: PanelInteractor,
- private val traceurMessageSender: TraceurMessageSender,
+ dialogTransitionAnimator: DialogTransitionAnimator,
+ panelInteractor: PanelInteractor,
+ traceurMessageSender: TraceurMessageSender,
private val issueRecordingState: IssueRecordingState,
- private val iActivityManager: IActivityManager,
+ iActivityManager: IActivityManager,
) :
RecordingService(
controller,
@@ -66,6 +64,18 @@
keyguardDismissUtil
) {
+ private val commandHandler =
+ IssueRecordingServiceCommandHandler(
+ bgExecutor,
+ dialogTransitionAnimator,
+ panelInteractor,
+ traceurMessageSender,
+ issueRecordingState,
+ iActivityManager,
+ notificationManager,
+ userContextProvider,
+ )
+
override fun getTag(): String = TAG
override fun getChannelId(): String = CHANNEL_ID
@@ -76,10 +86,7 @@
Log.d(getTag(), "handling action: ${intent?.action}")
when (intent?.action) {
ACTION_START -> {
- bgExecutor.execute {
- traceurMessageSender.startTracing(issueRecordingState.traceConfig)
- }
- issueRecordingState.isRecording = true
+ commandHandler.handleStartCommand()
if (!issueRecordingState.recordScreen) {
// If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
// will circumvent the RecordingService's screen recording start code.
@@ -87,41 +94,13 @@
}
}
ACTION_STOP,
- ACTION_STOP_NOTIF -> {
- // ViewCapture needs to save it's data before it is disabled, or else the data will
- // be lost. This is expected to change in the near future, and when that happens
- // this line should be removed.
- bgExecutor.execute {
- if (issueRecordingState.traceConfig.longTrace) {
- Settings.Global.putInt(
- contentResolver,
- NOTIFY_SESSION_ENDED_SETTING,
- DISABLED
- )
- }
- traceurMessageSender.stopTracing()
- }
- issueRecordingState.isRecording = false
- }
+ ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver)
ACTION_SHARE -> {
- bgExecutor.execute {
- mNotificationManager.cancelAsUser(
- null,
- intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
- UserHandle(mUserContextTracker.userContext.userId)
- )
-
- val screenRecording = intent.getParcelableExtra(EXTRA_PATH, Uri::class.java)
- if (issueRecordingState.takeBugreport) {
- iActivityManager.requestBugReportWithExtraAttachment(screenRecording)
- } else {
- traceurMessageSender.shareTraces(applicationContext, screenRecording)
- }
- }
-
- dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
- panelInteractor.collapsePanels()
-
+ commandHandler.handleShareCommand(
+ intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
+ intent.getParcelableExtra(EXTRA_PATH, Uri::class.java),
+ this
+ )
// Unlike all other actions, action_share has different behavior for the screen
// recording qs tile than it does for the record issue qs tile. Return sticky to
// avoid running any of the base class' code for this action.
@@ -135,8 +114,6 @@
companion object {
private const val TAG = "IssueRecordingService"
private const val CHANNEL_ID = "issue_record"
- private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
- private const val DISABLED = 0
/**
* Get an intent to stop the issue recording service.
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
new file mode 100644
index 0000000..32de0f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.app.IActivityManager
+import android.app.NotificationManager
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.settings.UserContextProvider
+import java.util.concurrent.Executor
+
+private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
+private const val DISABLED = 0
+
+/**
+ * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android
+ * specifically calls out that there is no supported way to test IntentServices here:
+ * https://developer.android.com/training/testing/other-components/services
+ */
+class IssueRecordingServiceCommandHandler(
+ private val bgExecutor: Executor,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val panelInteractor: PanelInteractor,
+ private val traceurMessageSender: TraceurMessageSender,
+ private val issueRecordingState: IssueRecordingState,
+ private val iActivityManager: IActivityManager,
+ private val notificationManager: NotificationManager,
+ private val userContextProvider: UserContextProvider,
+) {
+
+ fun handleStartCommand() {
+ bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) }
+ issueRecordingState.isRecording = true
+ }
+
+ fun handleStopCommand(contentResolver: ContentResolver) {
+ bgExecutor.execute {
+ if (issueRecordingState.traceConfig.longTrace) {
+ Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED)
+ }
+ traceurMessageSender.stopTracing()
+ }
+ issueRecordingState.isRecording = false
+ }
+
+ fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) {
+ bgExecutor.execute {
+ notificationManager.cancelAsUser(
+ null,
+ notificationId,
+ UserHandle(userContextProvider.userContext.userId)
+ )
+
+ if (issueRecordingState.takeBugreport) {
+ iActivityManager.requestBugReportWithExtraAttachment(screenRecording)
+ } else {
+ traceurMessageSender.shareTraces(context, screenRecording)
+ }
+ }
+
+ dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+ panelInteractor.collapsePanels()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 00944b8..834db98 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -119,5 +122,15 @@
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 4061ad8..a4c7d00 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -129,5 +132,15 @@
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 4c6341b..5482394 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,9 +19,11 @@
import android.view.MotionEvent
import androidx.compose.runtime.getValue
import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SwipeSourceDetector
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
@@ -33,12 +35,15 @@
import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Overlay
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -47,6 +52,8 @@
private val sceneInteractor: SceneInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
+ private val shadeInteractor: ShadeInteractor,
+ private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -59,6 +66,20 @@
/** Whether the container is visible. */
val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
+ /**
+ * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+ * [UserAction]s for this container.
+ */
+ val edgeDetector: SwipeSourceDetector by
+ hydrator.hydratedStateOf(
+ traceName = "edgeDetector",
+ initialValue = DefaultEdgeDetector,
+ source =
+ shadeInteractor.shadeMode.map {
+ if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+ }
+ )
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
new file mode 100644
index 0000000..f88bcb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/**
+ * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
+ * top-left and top-right.
+ */
+enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
+ SwipeSource {
+ TopLeft(resolveEdge = { Resolved.TopLeft }),
+ TopRight(resolveEdge = { Resolved.TopRight }),
+ TopStart(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
+ ),
+ TopEnd(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
+ ),
+ Bottom(resolveEdge = { Resolved.Bottom }),
+ Left(resolveEdge = { Resolved.Left }),
+ Right(resolveEdge = { Resolved.Right }),
+ Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+ End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+ override fun resolve(layoutDirection: LayoutDirection): Resolved {
+ return resolveEdge(layoutDirection)
+ }
+
+ enum class Resolved : SwipeSource.Resolved {
+ TopLeft,
+ TopRight,
+ Bottom,
+ Left,
+ Right,
+ }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
+ * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
+ * may change during runtime.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
+ * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
+ * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
+ * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
+ * [SceneContainerEdge.Resolved.Right].
+ *
+ * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
+ * percentage) of screen width to consider the split point between "top-left" and "top-right"
+ * edges. It is called on each source detection event.
+ * @param edgeSize The fixed size of each edge.
+ */
+class SplitEdgeDetector(
+ val topEdgeSplitFraction: () -> Float,
+ val edgeSize: Dp,
+) : SwipeSourceDetector {
+
+ private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+ override fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SceneContainerEdge.Resolved? {
+ val fixedEdge =
+ fixedEdgeDetector.source(
+ layoutSize,
+ position,
+ density,
+ orientation,
+ )
+ return when (fixedEdge) {
+ Edge.Resolved.Top -> {
+ val topEdgeSplitFraction = topEdgeSplitFraction()
+ require(topEdgeSplitFraction in 0f..1f) {
+ "topEdgeSplitFraction must return a value between 0.0 and 1.0"
+ }
+ val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
+ if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
+ else SceneContainerEdge.Resolved.TopRight
+ }
+ Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
+ Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
+ Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
+ null -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index ed590c3..553d1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -103,11 +103,8 @@
override val userContentResolver: ContentResolver
get() = userContext.contentResolver
- override val userInfo: UserInfo
- get() {
- val user = userId
- return userProfiles.first { it.id == user }
- }
+ override var userInfo: UserInfo by SynchronizedDelegate(UserInfo(context.userId, "", 0))
+ protected set
/**
* Returns a [List<UserInfo>] of all profiles associated with the current user.
@@ -187,6 +184,7 @@
userHandle = handle
userContext = ctx
userProfiles = profiles.map { UserInfo(it) }
+ userInfo = profiles.first { it.id == user }
}
return ctx to profiles
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 830649b..4ed4af6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -42,6 +42,7 @@
import android.util.MathUtils;
import android.view.MotionEvent;
import android.view.VelocityTracker;
+import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -463,6 +464,9 @@
mJavaAdapter.alwaysCollectFlow(
mCommunalTransitionViewModelLazy.get().isUmoOnCommunal(),
this::setShouldUpdateSquishinessOnMedia);
+ mJavaAdapter.alwaysCollectFlow(
+ mShadeInteractor.isAnyExpanded(),
+ this::onAnyExpandedChanged);
}
private void initNotificationStackScrollLayoutController() {
@@ -482,6 +486,10 @@
}
}
+ private void onAnyExpandedChanged(boolean isAnyExpanded) {
+ mQsFrame.setVisibility(isAnyExpanded ? View.VISIBLE : View.INVISIBLE);
+ }
+
private void onNotificationScrolled(int newScrollPosition) {
updateExpansionEnabledAmbient();
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 73e86a2..3cd91be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import androidx.annotation.FloatRange
import com.android.systemui.shade.shared.model.ShadeMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -69,6 +70,20 @@
* wide as the entire screen.
*/
val isShadeLayoutWide: StateFlow<Boolean>
+
+ /**
+ * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
+ * between "top-left" and "top-right" for the purposes of dual-shade invocation.
+ *
+ * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On
+ * wide layouts however, a larger fraction is returned because only the area of the system
+ * status icons is considered top-right.
+ *
+ * Note that this fraction only determines the split between the absolute left and right
+ * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
+ * will resolve to "top-left".
+ */
+ @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
}
/** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index d51fd28..6c0b55a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -47,4 +47,6 @@
override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
+
+ override fun getTopEdgeSplitFraction(): Float = 0.5f
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 3552092..b8d2dd2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import androidx.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -104,6 +105,16 @@
override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide
+ @FloatRange(from = 0.0, to = 1.0)
+ override fun getTopEdgeSplitFraction(): Float {
+ // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This
+ // assumption allows us to query its value on demand (during swipe source detection) instead
+ // of running another infinite coroutine.
+ // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based
+ // on the position of system-status icons in the status bar.
+ return if (shadeRepository.isShadeLayoutWide.value) 0.8f else 0.5f
+ }
+
override val shadeMode: StateFlow<ShadeMode> =
isShadeLayoutWide
.map(this::determineShadeMode)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
index af21e75..d36412c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
@@ -16,17 +16,23 @@
package com.android.systemui.statusbar.notification.data
+import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.SecureSettingsRepositoryModule
import com.android.systemui.settings.SystemSettingsRepositoryModule
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger
import dagger.Module
import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
@Module(includes = [SecureSettingsRepositoryModule::class, SystemSettingsRepositoryModule::class])
object NotificationSettingsRepositoryModule {
@@ -42,6 +48,19 @@
backgroundScope,
backgroundDispatcher,
secureSettingsRepository,
- systemSettingsRepository
- )
+ systemSettingsRepository)
+
+ @Provides
+ @IntoMap
+ @ClassKey(NotificationSettingsRepository::class)
+ @SysUISingleton
+ fun provideCoreStartable(
+ @Application applicationScope: CoroutineScope,
+ repository: NotificationSettingsRepository,
+ logger: VisualInterruptionDecisionLogger
+ ) = CoreStartable {
+ applicationScope.launch {
+ repository.isCooldownEnabled.collect { value -> logger.logCooldownSetting(value) }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index b83259d..38cab82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -102,6 +102,15 @@
{ "AvalancheSuppressor: $str1" }
)
}
+
+ fun logCooldownSetting(isEnabled: Boolean) {
+ buffer.log(
+ TAG,
+ INFO,
+ { bool1 = isEnabled },
+ { "Cooldown enabled: $bool1" }
+ )
+ }
}
private const val TAG = "VisualInterruptionDecisionProvider"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 580431a..969ff1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -68,6 +68,7 @@
if (mLabelTextId != null) {
mLabelView.setText(mLabelTextId);
}
+ mLabelView.setAccessibilityHeading(true);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index ef1bcfc..cccac4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -682,7 +682,10 @@
// doesn't get updated quickly enough and can cause the footer to flash when
// closing the shade. As such, we temporarily also check the ambientState directly.
if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
- viewState.hidden = true;
+ // Note: This is no longer necessary in flexiglass.
+ if (!SceneContainerFlag.isEnabled()) {
+ viewState.hidden = true;
+ }
} else {
final float footerEnd = algorithmState.mCurrentExpandedYPosition
+ view.getIntrinsicHeight();
@@ -691,7 +694,6 @@
noSpaceForFooter || (ambientState.isClearAllInProgress()
&& !hasNonClearableNotifs(algorithmState));
}
-
} else {
final boolean shadeClosed = !ambientState.isShadeExpanded();
final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index d770b20..dc9615c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -188,15 +188,26 @@
.startHistoryIntent(view, /* showHistory= */ true)
},
)
- launch {
- viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
- footerView.setVisible(
- /* visible = */ animatedVisibility.value,
- /* animate = */ animatedVisibility.isAnimating,
- )
+ if (SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.shouldShowFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
+ }
}
+ } else {
+ launch {
+ viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
+ footerView.setVisible(
+ /* visible = */ animatedVisibility.value,
+ /* animate = */ animatedVisibility.isAnimating,
+ )
+ }
+ }
+ launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
}
- launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
disposableHandle.awaitCancellationThenDispose()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index e55492e6..4e2a46d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -32,6 +32,7 @@
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.ui.AnimatableEvent
import com.android.systemui.util.ui.AnimatedValue
@@ -120,6 +121,7 @@
* This essentially corresponds to having the view set to INVISIBLE.
*/
val shouldHideFooterView: Flow<Boolean> by lazy {
+ SceneContainerFlag.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
} else {
@@ -143,6 +145,7 @@
* be hidden by another condition (see [shouldHideFooterView] above).
*/
val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ SceneContainerFlag.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(AnimatedValue.NotAnimating(false))
} else {
@@ -207,6 +210,76 @@
}
}
+ // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass.
+ val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ userSetupInteractor.isUserSetUp,
+ notificationStackInteractor.isShowingOnLockscreen,
+ shadeInteractor.isQsFullscreen,
+ remoteInputInteractor.isRemoteInputActive,
+ shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(),
+ ) {
+ hasNotifications,
+ isUserSetUp,
+ isShowingOnLockscreen,
+ qsFullScreen,
+ isRemoteInputActive,
+ shadeLessThanHalfwayExpanded ->
+ when {
+ !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Hide the footer until the user setup is complete, to prevent access
+ // to settings (b/193149550).
+ !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Do not show the footer if the lockscreen is visible (incl. AOD),
+ // except if the shade is opened on top. See also b/219680200.
+ // Do not animate, as that makes the footer appear briefly when
+ // transitioning between the shade and keyguard.
+ isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
+ // Do not show the footer if quick settings are fully expanded (except
+ // for the foldable split shade view). See b/201427195 && b/222699879.
+ qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // Hide the footer if remote input is active (i.e. user is replying to a
+ // notification). See b/75984847.
+ isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ // If the shade is not expanded enough, the footer shouldn't be visible.
+ shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+ else -> VisibilityChange.APPEAR_WITH_ANIMATION
+ }
+ }
+ .distinctUntilChanged(
+ // Equivalent unless visibility changes
+ areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
+ a.visible == b.visible
+ }
+ )
+ // Should we animate the visibility change?
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
+ // Animate if the shade is interactive, but NOT on the lockscreen. Having
+ // animations enabled while on the lockscreen makes the footer appear briefly
+ // when transitioning between the shade and keyguard.
+ val shouldAnimate =
+ isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
+ AnimatableEvent(visibilityChange.visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .dumpWhileCollecting("shouldShowFooterView")
+ .flowOn(bgDispatcher)
+ }
+ }
+
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index dd4b000..f3b9371 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -182,6 +182,7 @@
private boolean mBouncerShowingOverDream;
private int mAttemptsToShowBouncer = 0;
private DelayableExecutor mExecutor;
+ private boolean mIsSleeping = false;
private final PrimaryBouncerExpansionCallback mExpansionCallback =
new PrimaryBouncerExpansionCallback() {
@@ -713,7 +714,11 @@
* {@link #needsFullscreenBouncer()}.
*/
protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
- if (needsFullscreenBouncer() && !mDozing) {
+ boolean showBouncer = needsFullscreenBouncer() && !mDozing;
+ if (Flags.simPinRaceConditionOnRestart()) {
+ showBouncer = showBouncer && !mIsSleeping;
+ }
+ if (showBouncer) {
// The keyguard might be showing (already). So we need to hide it.
if (!primaryBouncerIsShowing()) {
if (SceneContainerFlag.isEnabled()) {
@@ -1041,6 +1046,7 @@
@Override
public void onStartedWakingUp() {
+ mIsSleeping = false;
setRootViewAnimationDisabled(false);
NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
if (navBarView != null) {
@@ -1054,6 +1060,7 @@
@Override
public void onStartedGoingToSleep() {
+ mIsSleeping = true;
setRootViewAnimationDisabled(true);
NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
if (navBarView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
index 9715772..28a43df 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
@@ -16,35 +16,16 @@
package com.android.systemui.volume.dagger
-import android.view.accessibility.CaptioningManager
import com.android.systemui.accessibility.data.repository.CaptioningRepository
import com.android.systemui.accessibility.data.repository.CaptioningRepositoryImpl
-import com.android.systemui.accessibility.domain.interactor.CaptioningInteractor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import dagger.Binds
import dagger.Module
-import dagger.Provides
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
@Module
interface CaptioningModule {
- companion object {
-
- @Provides
- @SysUISingleton
- fun provideCaptioningRepository(
- captioningManager: CaptioningManager,
- @Background coroutineContext: CoroutineContext,
- @Application coroutineScope: CoroutineScope,
- ): CaptioningRepository =
- CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope)
-
- @Provides
- @SysUISingleton
- fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor =
- CaptioningInteractor(repository)
- }
+ @Binds
+ @SysUISingleton
+ fun bindCaptioningRepository(impl: CaptioningRepositoryImpl): CaptioningRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
index 52f2ce6..2e5e389 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
@@ -26,7 +26,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
@VolumePanelScope
class CaptioningAvailabilityCriteria
@@ -45,7 +45,7 @@
else VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE
)
}
- .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
override fun isAvailable(): Flow<Boolean> = availability
}
diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
new file mode 100644
index 0000000..f37580d
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
@@ -0,0 +1,831 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304,
+ 320,
+ 336,
+ 352,
+ 368,
+ 384,
+ 400,
+ 416,
+ 432,
+ 448,
+ 464,
+ 480,
+ 496,
+ 512,
+ 528,
+ 544,
+ 560,
+ 576,
+ 592,
+ 608,
+ 624,
+ 640,
+ 656,
+ 672,
+ 688,
+ 704,
+ 720,
+ 736,
+ 752,
+ 768,
+ 784,
+ 800,
+ 816,
+ 832,
+ 848,
+ 864,
+ 880,
+ 896,
+ 912,
+ 928,
+ 944,
+ 960,
+ 976,
+ 992,
+ 1008,
+ 1024,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "content_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.9954499,
+ 0.9805035,
+ 0.9527822,
+ 0.9092045,
+ 0.84588075,
+ 0.7583043,
+ 0.6424476,
+ 0.49766344,
+ 0.33080608,
+ 0.15650165,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "content_scale",
+ "type": "scale",
+ "data_points": [
+ "default",
+ {
+ "x": 0.9995097,
+ "y": 0.9995097,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.997352,
+ "y": 0.997352,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.990635,
+ "y": 0.990635,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.97249764,
+ "y": 0.97249764,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.94287145,
+ "y": 0.94287145,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.9128026,
+ "y": 0.9128026,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8859569,
+ "y": 0.8859569,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8629254,
+ "y": 0.8629254,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8442908,
+ "y": 0.8442908,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8303209,
+ "y": 0.8303209,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8205137,
+ "y": 0.8205137,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.81387186,
+ "y": 0.81387186,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80941653,
+ "y": 0.80941653,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80641484,
+ "y": 0.80641484,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80437464,
+ "y": 0.80437464,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80297637,
+ "y": 0.80297637,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80201286,
+ "y": 0.80201286,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8013477,
+ "y": 0.8013477,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8008894,
+ "y": 0.8008894,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8005756,
+ "y": 0.8005756,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80036324,
+ "y": 0.80036324,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8002219,
+ "y": 0.8002219,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80012995,
+ "y": 0.80012995,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000721,
+ "y": 0.8000721,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.80003715,
+ "y": 0.80003715,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000173,
+ "y": 0.8000173,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.800007,
+ "y": 0.800007,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000022,
+ "y": 0.8000022,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8000004,
+ "y": 0.8000004,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.79999995,
+ "y": 0.79999995,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "x": 0.8,
+ "y": 0.8,
+ "pivot": "unspecified"
+ },
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "content_offset",
+ "type": "dpOffset",
+ "data_points": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 0,
+ "y": 0.5714286
+ },
+ {
+ "x": 0,
+ "y": 2.857143
+ },
+ {
+ "x": 0,
+ "y": 7.142857
+ },
+ {
+ "x": 0,
+ "y": 13.714286
+ },
+ {
+ "x": 0,
+ "y": 23.142857
+ },
+ {
+ "x": 0,
+ "y": 36.285713
+ },
+ {
+ "x": 0,
+ "y": 53.714287
+ },
+ {
+ "x": 0,
+ "y": 75.42857
+ },
+ {
+ "x": 0,
+ "y": 100.28571
+ },
+ {
+ "x": 0,
+ "y": 126.57143
+ },
+ {
+ "x": 0,
+ "y": 151.42857
+ },
+ {
+ "x": 0,
+ "y": 174
+ },
+ {
+ "x": 0,
+ "y": 193.42857
+ },
+ {
+ "x": 0,
+ "y": 210.28572
+ },
+ {
+ "x": 0,
+ "y": 224.85715
+ },
+ {
+ "x": 0,
+ "y": 237.14285
+ },
+ {
+ "x": 0,
+ "y": 247.71428
+ },
+ {
+ "x": 0,
+ "y": 256.85715
+ },
+ {
+ "x": 0,
+ "y": 264.57144
+ },
+ {
+ "x": 0,
+ "y": 271.42856
+ },
+ {
+ "x": 0,
+ "y": 277.14285
+ },
+ {
+ "x": 0,
+ "y": 282
+ },
+ {
+ "x": 0,
+ "y": 286.2857
+ },
+ {
+ "x": 0,
+ "y": 289.7143
+ },
+ {
+ "x": 0,
+ "y": 292.57144
+ },
+ {
+ "x": 0,
+ "y": 294.85715
+ },
+ {
+ "x": 0,
+ "y": 296.85715
+ },
+ {
+ "x": 0,
+ "y": 298.2857
+ },
+ {
+ "x": 0,
+ "y": 299.14285
+ },
+ {
+ "x": 0,
+ "y": 299.7143
+ },
+ {
+ "x": 0,
+ "y": 300
+ },
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "type": "not_found"
+ }
+ ]
+ },
+ {
+ "name": "background_alpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.9900334,
+ 0.8403853,
+ 0.71002257,
+ 0.5979084,
+ 0.50182605,
+ 0.41945767,
+ 0.34874845,
+ 0.28797746,
+ 0.23573697,
+ 0.19087732,
+ 0.1524564,
+ 0.11970067,
+ 0.091962695,
+ 0.068702936,
+ 0.049464583,
+ 0.033859253,
+ 0.021552086,
+ 0.012255073,
+ 0.005717635,
+ 0.0017191172,
+ 6.711483e-05,
+ 0,
+ {
+ "type": "not_found"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
new file mode 100644
index 0000000..22946c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import android.app.AlertDialog
+import android.platform.test.annotations.MotionTest
+import android.testing.TestableLooper.RunWithLooper
+import androidx.activity.BackEventCompat
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.isElement
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.UnknownTypeException
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+/** MotionTest for the Bouncer Predictive Back animation */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@EnableSceneContainer
+@MotionTest
+class BouncerPredictiveBackTest : SysuiTestCase() {
+
+ private val deviceSpec = DeviceEmulationSpec(Phone)
+ private val kosmos = testKosmos()
+
+ @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+ private val androidComposeTestRule =
+ motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
+
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
+ private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
+ private val Kosmos.sceneContainerConfig by Fixture {
+ val navigationDistances =
+ mapOf(
+ Scenes.Lockscreen to 1,
+ Scenes.Bouncer to 0,
+ )
+ SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
+ }
+
+ private val transitionState by lazy {
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
+ )
+ }
+ private val sceneContainerViewModel by lazy {
+ SceneContainerViewModel(
+ sceneInteractor = kosmos.sceneInteractor,
+ falsingInteractor = kosmos.falsingInteractor,
+ powerInteractor = kosmos.powerInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
+ splitEdgeDetector = kosmos.splitEdgeDetector,
+ logger = kosmos.sceneLogger,
+ motionEventHandlerReceiver = {},
+ )
+ .apply { setTransitionState(transitionState) }
+ }
+
+ private val bouncerDialogFactory =
+ object : BouncerDialogFactory {
+ override fun invoke(): AlertDialog {
+ throw AssertionError()
+ }
+ }
+ private val bouncerSceneActionsViewModelFactory =
+ object : BouncerUserActionsViewModel.Factory {
+ override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
+ }
+ private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
+ private val bouncerSceneContentViewModelFactory =
+ object : BouncerSceneContentViewModel.Factory {
+ override fun create() = bouncerSceneContentViewModel
+ }
+ private val bouncerScene =
+ BouncerScene(
+ bouncerSceneActionsViewModelFactory,
+ bouncerSceneContentViewModelFactory,
+ bouncerDialogFactory
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
+
+ val startable = kosmos.sceneContainerStartable
+ startable.start()
+ }
+
+ @Test
+ fun bouncerPredictiveBackMotion() =
+ motionTestRule.runTest {
+ val motion =
+ recordMotion(
+ content = { play ->
+ PlatformTheme {
+ BackGestureAnimation(play)
+ SceneContainer(
+ viewModel =
+ rememberViewModel("BouncerPredictiveBackTest") {
+ sceneContainerViewModel
+ },
+ sceneByKey =
+ mapOf(
+ Scenes.Lockscreen to FakeLockscreen(),
+ Scenes.Bouncer to bouncerScene
+ ),
+ initialSceneKey = Scenes.Bouncer,
+ overlayByKey = emptyMap(),
+ dataSourceDelegator = kosmos.sceneDataSourceDelegator
+ )
+ }
+ },
+ ComposeRecordingSpec(
+ MotionControl(
+ delayRecording = {
+ awaitCondition {
+ sceneInteractor.transitionState.value.isTransitioning()
+ }
+ }
+ ) {
+ awaitCondition {
+ sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
+ }
+ }
+ ) {
+ feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
+ feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
+ feature(
+ isElement(Bouncer.Elements.Content),
+ positionInRoot,
+ "content_offset"
+ )
+ feature(
+ isElement(Bouncer.Elements.Background),
+ elementAlpha,
+ "background_alpha"
+ )
+ }
+ )
+
+ assertThat(motion).timeSeriesMatchesGolden()
+ }
+
+ @Composable
+ private fun BackGestureAnimation(play: Boolean) {
+ val backProgress = remember { Animatable(0f) }
+
+ LaunchedEffect(play) {
+ if (play) {
+ val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
+ androidComposeTestRule.runOnUiThread {
+ dispatcher.dispatchOnBackStarted(backEvent())
+ }
+ backProgress.animateTo(
+ targetValue = 1f,
+ animationSpec = tween(durationMillis = 500)
+ ) {
+ androidComposeTestRule.runOnUiThread {
+ dispatcher.dispatchOnBackProgressed(
+ backEvent(progress = backProgress.value)
+ )
+ if (backProgress.value == 1f) {
+ dispatcher.onBackPressed()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun backEvent(progress: Float = 0f): BackEventCompat {
+ return BackEventCompat(
+ touchX = 0f,
+ touchY = 0f,
+ progress = progress,
+ swipeEdge = BackEventCompat.EDGE_LEFT,
+ )
+ }
+
+ private class FakeLockscreen : ExclusiveActivatable(), Scene {
+ override val key: SceneKey = Scenes.Lockscreen
+ override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
+
+ @Composable
+ override fun SceneScope.Content(modifier: Modifier) {
+ Box(modifier = modifier, contentAlignment = Alignment.Center) {
+ Text(text = "Fake Lockscreen")
+ }
+ }
+
+ override suspend fun onActivated() = awaitCancellation()
+ }
+
+ companion object {
+ private val elementAlpha =
+ FeatureCapture<SemanticsNode, Float>("alpha") {
+ DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+ }
+
+ private val elementScale =
+ FeatureCapture<SemanticsNode, Scale>("scale") {
+ DataPoint.of(it.lastScaleForTesting, scale)
+ }
+
+ private val scale: DataPointType<Scale> =
+ DataPointType(
+ "scale",
+ jsonToValue = {
+ when (it) {
+ "unspecified" -> Scale.Unspecified
+ "default" -> Scale.Default
+ "zero" -> Scale.Zero
+ is JSONObject -> {
+ val pivot = it.get("pivot")
+ Scale(
+ scaleX = it.getDouble("x").toFloat(),
+ scaleY = it.getDouble("y").toFloat(),
+ pivot =
+ when (pivot) {
+ "unspecified" -> Offset.Unspecified
+ "infinite" -> Offset.Infinite
+ is JSONObject ->
+ Offset(
+ pivot.getDouble("x").toFloat(),
+ pivot.getDouble("y").toFloat()
+ )
+ else -> throw UnknownTypeException()
+ }
+ )
+ }
+ else -> throw UnknownTypeException()
+ }
+ },
+ valueToJson = {
+ when (it) {
+ Scale.Unspecified -> "unspecified"
+ Scale.Default -> "default"
+ Scale.Zero -> "zero"
+ else -> {
+ JSONObject().apply {
+ put("x", it.scaleX)
+ put("y", it.scaleY)
+ put(
+ "pivot",
+ when {
+ it.pivot.isUnspecified -> "unspecified"
+ !it.pivot.isFinite -> "infinite"
+ else ->
+ JSONObject().apply {
+ put("x", it.pivot.x)
+ put("y", it.pivot.y)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index ad7a5b6..3c74374 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -967,7 +967,8 @@
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
assertThat(data.app).isEqualTo(APP_NAME)
- assertThat(data.actions).hasSize(1)
+ // resume button is a semantic action.
+ assertThat(data.actions).hasSize(0)
assertThat(data.semanticActions!!.playOrPause).isNotNull()
assertThat(data.lastActive).isAtLeast(currentTime)
verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -994,7 +995,8 @@
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
assertThat(data.app).isEqualTo(APP_NAME)
- assertThat(data.actions).hasSize(1)
+ // resume button is a semantic action.
+ assertThat(data.actions).hasSize(0)
assertThat(data.semanticActions!!.playOrPause).isNotNull()
assertThat(data.lastActive).isAtLeast(currentTime)
assertThat(data.isExplicit).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index c0f503d..4cf7de3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -69,6 +69,8 @@
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.mediaLogger
+import com.android.systemui.media.controls.shared.mockMediaLogger
import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.shared.model.MediaData
@@ -140,7 +142,7 @@
@RunWith(ParameterizedAndroidJunit4::class)
@EnableSceneContainer
class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger }
private val testDispatcher = kosmos.testDispatcher
private val testScope = kosmos.testScope
private val settings = kosmos.fakeSettings
@@ -257,6 +259,7 @@
keyguardUpdateMonitor = keyguardUpdateMonitor,
mediaDataRepository = kosmos.mediaDataRepository,
mediaDataLoader = { kosmos.mediaDataLoader },
+ mediaLogger = kosmos.mediaLogger,
)
mediaDataProcessor.start()
testScope.runCurrent()
@@ -984,7 +987,8 @@
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
assertThat(data.app).isEqualTo(APP_NAME)
- assertThat(data.actions).hasSize(1)
+ // resume button is a semantic action.
+ assertThat(data.actions).hasSize(0)
assertThat(data.semanticActions!!.playOrPause).isNotNull()
assertThat(data.lastActive).isAtLeast(currentTime)
verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -1011,7 +1015,8 @@
assertThat(data.resumption).isTrue()
assertThat(data.song).isEqualTo(SESSION_TITLE)
assertThat(data.app).isEqualTo(APP_NAME)
- assertThat(data.actions).hasSize(1)
+ // resume button is a semantic action.
+ assertThat(data.actions).hasSize(0)
assertThat(data.semanticActions!!.playOrPause).isNotNull()
assertThat(data.lastActive).isAtLeast(currentTime)
assertThat(data.isExplicit).isTrue()
@@ -2476,6 +2481,55 @@
assertThat(mediaDataCaptor.value.artwork).isNull()
}
+ @Test
+ @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+ fun postDuplicateNotification_doesNotCallListeners() {
+ whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+ whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+
+ mediaDataProcessor.addInternalListener(mediaDataFilter)
+ mediaDataFilter.mediaDataProcessor = mediaDataProcessor
+ addNotificationAndLoad()
+ reset(listener)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+
+ testScope.assertRunAllReady(foreground = 0, background = 1)
+ verify(listener, never())
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ verify(kosmos.mediaLogger).logDuplicateMediaNotification(eq(KEY))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+ fun postDuplicateNotification_callsListeners() {
+ whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+ whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+
+ mediaDataProcessor.addInternalListener(mediaDataFilter)
+ mediaDataFilter.mediaDataProcessor = mediaDataProcessor
+ addNotificationAndLoad()
+ reset(listener)
+ mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+ testScope.assertRunAllReady(foreground = 1, background = 1)
+ verify(listener)
+ .onMediaDataLoaded(
+ eq(KEY),
+ eq(KEY),
+ capture(mediaDataCaptor),
+ eq(true),
+ eq(0),
+ eq(false)
+ )
+ verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
+ }
+
private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) {
runCurrent()
if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index c1bba4d..680df15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -72,7 +72,6 @@
@Mock private lateinit var mediaController: MediaController
@Mock private lateinit var logger: MediaTimeoutLogger
@Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
- private lateinit var executor: FakeExecutor
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
@Mock private lateinit var sessionCallback: (String) -> Unit
@@ -88,6 +87,9 @@
private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener
private var clock = FakeSystemClock()
+ private lateinit var mainExecutor: FakeExecutor
+ private lateinit var bgExecutor: FakeExecutor
+ private lateinit var uiExecutor: FakeExecutor
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@@ -95,11 +97,15 @@
fun setup() {
whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
- executor = FakeExecutor(clock)
+ mainExecutor = FakeExecutor(clock)
+ bgExecutor = FakeExecutor(clock)
+ uiExecutor = FakeExecutor(clock)
mediaTimeoutListener =
MediaTimeoutListener(
mediaControllerFactory,
- executor,
+ bgExecutor,
+ uiExecutor,
+ mainExecutor,
logger,
statusBarStateController,
clock,
@@ -143,30 +149,31 @@
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(mediaController.playbackState).thenReturn(playingState)
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
verify(logger).logPlaybackState(eq(KEY), eq(playingState))
// Ignores if same key
clearInvocations(mediaController)
- mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
+ loadMediaData(KEY, KEY, mediaData)
verify(mediaController, never()).registerCallback(anyObject())
}
@Test
fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false))
- assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataRemoved_unregistersPlaybackListener() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
mediaTimeoutListener.onMediaDataRemoved(KEY, false)
+ assertThat(bgExecutor.runAllReady()).isEqualTo(1)
verify(mediaController).unregisterCallback(anyObject())
// Ignores duplicate requests
@@ -178,50 +185,50 @@
@Test
fun testOnMediaDataRemoved_clearsTimeout() {
// GIVEN media that is paused
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
- assertThat(executor.numPending()).isEqualTo(1)
+ loadMediaData(KEY, null, mediaData)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// WHEN the media is removed
mediaTimeoutListener.onMediaDataRemoved(KEY, false)
// THEN the timeout runnable is cancelled
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
}
@Test
fun testOnMediaDataLoaded_migratesKeys() {
val newKey = "NEWKEY"
// From not playing
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
clearInvocations(mediaController)
// To playing
val playingState = mock(android.media.session.PlaybackState::class.java)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(mediaController.playbackState).thenReturn(playingState)
- mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+ loadMediaData(newKey, KEY, mediaData)
verify(mediaController).unregisterCallback(anyObject())
verify(mediaController).registerCallback(anyObject())
verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true))
// Enqueues callback
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
}
@Test
fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
val newKey = "NEWKEY"
// From not playing
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
clearInvocations(mediaController)
// Migrate, still not playing
val playingState = mock(android.media.session.PlaybackState::class.java)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
whenever(mediaController.playbackState).thenReturn(playingState)
- mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+ loadMediaData(newKey, KEY, mediaData)
// The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
// is another scheduled
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(logger).logUpdateListener(eq(newKey), eq(false))
}
@@ -233,8 +240,8 @@
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
- assertThat(executor.numPending()).isEqualTo(1)
- assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
}
@Test
@@ -245,7 +252,7 @@
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
)
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
verify(logger).logTimeoutCancelled(eq(KEY), any())
}
@@ -257,7 +264,7 @@
mediaCallbackCaptor.value.onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
}
@Test
@@ -265,7 +272,7 @@
// Assuming we're have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- with(executor) {
+ with(mainExecutor) {
advanceClockToNext()
runAllReady()
}
@@ -274,7 +281,7 @@
@Test
fun testIsTimedOut() {
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
}
@@ -282,16 +289,17 @@
fun testOnSessionDestroyed_active_clearsTimeout() {
// GIVEN media that is paused
val mediaPaused = mediaData.copy(isPlaying = false)
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
+ loadMediaData(KEY, null, mediaPaused)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// WHEN the session is destroyed
mediaCallbackCaptor.value.onSessionDestroyed()
// THEN the controller is unregistered and timeout run
+ assertThat(bgExecutor.runAllReady()).isEqualTo(1)
verify(mediaController).unregisterCallback(anyObject())
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
verify(logger).logSessionDestroyed(eq(KEY))
verify(sessionCallback).invoke(eq(KEY))
}
@@ -306,11 +314,11 @@
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(mediaController.playbackState).thenReturn(playingState)
val mediaPlaying = mediaData.copy(isPlaying = true)
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
+ loadMediaData(KEY, null, mediaPlaying)
// THEN the timeout runnable will update the state
- assertThat(executor.numPending()).isEqualTo(1)
- with(executor) {
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
+ with(mainExecutor) {
advanceClockToNext()
runAllReady()
}
@@ -322,31 +330,32 @@
fun testOnSessionDestroyed_resume_continuesTimeout() {
// GIVEN resume media with session info
val resumeWithSession = resumeData.copy(token = session.sessionToken)
- mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
+ loadMediaData(PACKAGE, null, resumeWithSession)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// WHEN the session is destroyed
mediaCallbackCaptor.value.onSessionDestroyed()
// THEN the controller is unregistered, but the timeout is still scheduled
+ assertThat(bgExecutor.runAllReady()).isEqualTo(1)
verify(mediaController).unregisterCallback(anyObject())
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(sessionCallback, never()).invoke(eq(KEY))
}
@Test
fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
// WHEN a regular media is loaded
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(KEY, null, mediaData)
// AND it turns into a resume control
- mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+ loadMediaData(PACKAGE, KEY, resumeData)
// THEN we register a timeout
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
- assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
@@ -355,42 +364,42 @@
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
whenever(mediaController.playbackState).thenReturn(pausedState)
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
- assertThat(executor.numPending()).isEqualTo(1)
+ loadMediaData(KEY, null, mediaData)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// AND it turns into a resume control
- mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+ loadMediaData(PACKAGE, KEY, resumeData)
// THEN we update the timeout length
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
- assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataLoaded_resumption_registersTimeout() {
// WHEN a resume media is loaded
- mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+ loadMediaData(PACKAGE, null, resumeData)
// THEN we register a timeout
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
- assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
}
@Test
fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
// WHEN we have a resume control
- mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+ loadMediaData(PACKAGE, null, resumeData)
// AND that media is resumed
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
whenever(mediaController.playbackState).thenReturn(playingState)
- mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
+ loadMediaData(oldKey = PACKAGE, data = mediaData)
// THEN the timeout length is changed to a regular media control
- assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
}
@Test
@@ -401,7 +410,7 @@
mediaTimeoutListener.onMediaDataRemoved(PACKAGE, false)
// THEN the timeout runnable is cancelled
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
}
@Test
@@ -427,6 +436,7 @@
// When the playback state changes, and has different actions
val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
@@ -463,6 +473,7 @@
.addCustomAction(customTwo)
.build()
mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+ assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
@@ -534,6 +545,7 @@
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ uiExecutor.runAllReady()
// Then the callback is invoked
verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
@@ -567,7 +579,7 @@
// And we doze past the scheduled timeout
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// Then when no longer dozing, the timeout runs immediately
dozingCallbackCaptor.value.onDozingChanged(false)
@@ -576,7 +588,7 @@
// and cancel any later scheduled timeout
verify(logger).logTimeoutCancelled(eq(KEY), any())
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
}
@Test
@@ -592,12 +604,12 @@
// And we doze, but not past the scheduled timeout
clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// Then when no longer dozing, the timeout remains scheduled
dozingCallbackCaptor.value.onDozingChanged(false)
verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
}
@Test
@@ -610,8 +622,8 @@
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- assertThat(executor.numPending()).isEqualTo(1)
- assertThat(executor.advanceClockToNext()).isEqualTo(duration)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration)
}
@Test
@@ -619,7 +631,7 @@
// Given a pending timeout
testSmartspaceDataLoaded_schedulesTimeout()
- executor.runAllReady()
+ mainExecutor.runAllReady()
verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
}
@@ -634,14 +646,14 @@
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
val expiryLonger = expireTime + duration
whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- assertThat(executor.numPending()).isEqualTo(1)
- assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration * 2)
}
@Test
@@ -649,10 +661,10 @@
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
}
@Test
@@ -667,12 +679,12 @@
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// And we doze past the scheduled timeout
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time + duration * 2)
- assertThat(executor.numPending()).isEqualTo(1)
+ assertThat(mainExecutor.numPending()).isEqualTo(1)
// Then when no longer dozing, the timeout runs immediately
dozingCallbackCaptor.value.onDozingChanged(false)
@@ -680,12 +692,18 @@
verify(logger).logTimeout(eq(SMARTSPACE_KEY))
// and cancel any later scheduled timeout
- assertThat(executor.numPending()).isEqualTo(0)
+ assertThat(mainExecutor.numPending()).isEqualTo(0)
}
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
whenever(mediaController.playbackState).thenReturn(state)
- mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+ loadMediaData(data = mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
}
+
+ private fun loadMediaData(key: String = KEY, oldKey: String? = null, data: MediaData) {
+ mediaTimeoutListener.onMediaDataLoaded(key, oldKey, data)
+ bgExecutor.runAllReady()
+ uiExecutor.runAllReady()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 1260a65..68a5d93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -79,6 +79,7 @@
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.ui.binder.SeekBarObserver
import com.android.systemui.media.controls.ui.view.GutsViewHolder
@@ -236,6 +237,19 @@
@Mock private lateinit var recProgressBar3: SeekBar
@Mock private lateinit var globalSettings: GlobalSettings
+ private val intent =
+ Intent().apply {
+ putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) })
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ private val pendingIntent =
+ PendingIntent.getActivity(
+ mContext,
+ 0,
+ intent.setPackage(mContext.packageName),
+ PendingIntent.FLAG_MUTABLE
+ )
+
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Before
@@ -989,14 +1003,13 @@
@Test
fun bindNotificationActions() {
val icon = context.getDrawable(android.R.drawable.ic_media_play)
- val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
val actions =
listOf(
- MediaAction(icon, Runnable {}, "previous", bg),
- MediaAction(icon, Runnable {}, "play", bg),
- MediaAction(icon, null, "next", bg),
- MediaAction(icon, null, "custom 0", bg),
- MediaAction(icon, Runnable {}, "custom 1", bg)
+ MediaNotificationAction(true, actionIntent = pendingIntent, icon, "previous"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"),
+ MediaNotificationAction(true, actionIntent = null, icon, "next"),
+ MediaNotificationAction(true, actionIntent = null, icon, "custom 0"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1")
)
val state =
mediaData.copy(
@@ -1684,11 +1697,11 @@
fun actionCustom2Click_isLogged() {
val actions =
listOf(
- MediaAction(null, Runnable {}, "action 0", null),
- MediaAction(null, Runnable {}, "action 1", null),
- MediaAction(null, Runnable {}, "action 2", null),
- MediaAction(null, Runnable {}, "action 3", null),
- MediaAction(null, Runnable {}, "action 4", null)
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
)
val data = mediaData.copy(actions = actions)
@@ -1703,11 +1716,11 @@
fun actionCustom3Click_isLogged() {
val actions =
listOf(
- MediaAction(null, Runnable {}, "action 0", null),
- MediaAction(null, Runnable {}, "action 1", null),
- MediaAction(null, Runnable {}, "action 2", null),
- MediaAction(null, Runnable {}, "action 3", null),
- MediaAction(null, Runnable {}, "action 4", null)
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
)
val data = mediaData.copy(actions = actions)
@@ -1722,11 +1735,11 @@
fun actionCustom4Click_isLogged() {
val actions =
listOf(
- MediaAction(null, Runnable {}, "action 0", null),
- MediaAction(null, Runnable {}, "action 1", null),
- MediaAction(null, Runnable {}, "action 2", null),
- MediaAction(null, Runnable {}, "action 3", null),
- MediaAction(null, Runnable {}, "action 4", null)
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+ MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
)
val data = mediaData.copy(actions = actions)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index c1cf91d..bc0ec2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -22,6 +22,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
+import static com.android.systemui.Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES;
import static com.google.common.truth.Truth.assertThat;
@@ -75,6 +76,8 @@
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
+import com.google.common.truth.Truth;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -95,7 +98,8 @@
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+ return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+ FLAG_QS_QUICK_REBIND_ACTIVE_TILES);
}
private final PackageManagerAdapter mMockPackageManagerAdapter =
@@ -154,7 +158,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
}
@After
@@ -169,12 +174,12 @@
mStateManager.handleDestroy();
}
- private void setPackageEnabled(boolean enabled) throws Exception {
+ private void setPackageEnabledAndActive(boolean enabled, boolean active) throws Exception {
ServiceInfo defaultServiceInfo = null;
if (enabled) {
defaultServiceInfo = new ServiceInfo();
defaultServiceInfo.metaData = new Bundle();
- defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, true);
+ defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active);
defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, true);
}
when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), anyInt()))
@@ -186,6 +191,10 @@
.thenReturn(defaultPackageInfo);
}
+ private void setPackageEnabled(boolean enabled) throws Exception {
+ setPackageEnabledAndActive(enabled, true);
+ }
+
private void setPackageInstalledForUser(
boolean installed,
boolean active,
@@ -396,13 +405,40 @@
}
@Test
- public void testKillProcess() throws Exception {
+ public void testKillProcessWhenTileServiceIsNotActive() throws Exception {
+ setPackageEnabledAndActive(true, false);
mStateManager.onStartListening();
mStateManager.executeSetBindService(true);
mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
mStateManager.onBindingDied(mTileServiceComponentName);
mExecutor.runAllReady();
- mClock.advanceTime(5000);
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ // still 4 seconds left because non active tile service rebind time is 5 seconds
+ Truth.assertThat(mContext.isBound(mTileServiceComponentName)).isFalse();
+
+ mClock.advanceTime(4000); // 5 seconds delay for nonActive service rebinding
+ mExecutor.runAllReady();
+ verifyBind(2);
+ verify(mMockTileService, times(2)).onStartListening();
+ }
+
+ @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActive_withRebindFlagOn() throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
mExecutor.runAllReady();
// Two calls: one for the first bind, one for the restart.
@@ -410,6 +446,86 @@
verify(mMockTileService, times(2)).onStartListening();
}
+ @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActive_withRebindFlagOff() throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+ verifyBind(0); // the rebind happens after 4 more seconds
+
+ mClock.advanceTime(4000);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ }
+
+ @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOn_delaysSecondRebind()
+ throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ // Two calls: one for the first bind, one for the restart.
+ verifyBind(2);
+ verify(mMockTileService, times(2)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName);
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+ // because active tile will take 5 seconds to bind the second time, not 1
+ verifyBind(0);
+
+ mClock.advanceTime(4000);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ }
+
+ @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+ @Test
+ public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOff_rebindsFromFirstKill()
+ throws Exception {
+ mStateManager.onStartListening();
+ mStateManager.executeSetBindService(true);
+ mExecutor.runAllReady();
+ verifyBind(1);
+ verify(mMockTileService, times(1)).onStartListening();
+
+ mStateManager.onBindingDied(mTileServiceComponentName); // rebind scheduled for 5 seconds
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ verifyBind(0); // it would bind in 4 more seconds
+
+ mStateManager.onBindingDied(mTileServiceComponentName); // this does not affect the rebind
+ mExecutor.runAllReady();
+ mClock.advanceTime(1000);
+ mExecutor.runAllReady();
+
+ verifyBind(0); // only 2 seconds passed from first kill
+
+ mClock.advanceTime(3000);
+ mExecutor.runAllReady();
+ verifyBind(1); // the rebind scheduled 5 seconds from the first kill should now happen
+ }
+
@Test
public void testKillProcessLowMemory() throws Exception {
doAnswer(invocation -> {
@@ -510,7 +626,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -533,7 +650,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -556,7 +674,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -581,7 +700,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
manager.executeSetBindService(true);
mExecutor.runAllReady();
@@ -607,7 +727,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isActiveTile()).isTrue();
}
@@ -626,7 +747,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isActiveTile()).isTrue();
}
@@ -644,7 +766,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isTrue();
}
@@ -663,7 +786,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isTrue();
}
@@ -682,7 +806,8 @@
mUser,
mActivityManager,
mDeviceIdleController,
- mExecutor);
+ mExecutor,
+ mClock);
assertThat(manager.isToggleableTile()).isFalse();
assertThat(manager.isActiveTile()).isFalse();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
new file mode 100644
index 0000000..57cfe1b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.app.IActivityManager
+import android.app.NotificationManager
+import android.net.Uri
+import android.os.UserHandle
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userFileManager
+import com.android.systemui.settings.userTracker
+import com.android.traceur.TraceConfig
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos().also { it.testCase = this }
+ private val bgExecutor = kosmos.fakeExecutor
+ private val userContextProvider: UserContextProvider = kosmos.userTracker
+ private val dialogTransitionAnimator: DialogTransitionAnimator = kosmos.dialogTransitionAnimator
+ private lateinit var traceurMessageSender: TraceurMessageSender
+ private val issueRecordingState =
+ IssueRecordingState(kosmos.userTracker, kosmos.userFileManager)
+
+ private val iActivityManager = mock<IActivityManager>()
+ private val notificationManager = mock<NotificationManager>()
+ private val panelInteractor = mock<PanelInteractor>()
+
+ private lateinit var underTest: IssueRecordingServiceCommandHandler
+
+ @Before
+ fun setup() {
+ traceurMessageSender = mock<TraceurMessageSender>()
+ underTest =
+ IssueRecordingServiceCommandHandler(
+ bgExecutor,
+ dialogTransitionAnimator,
+ panelInteractor,
+ traceurMessageSender,
+ issueRecordingState,
+ iActivityManager,
+ notificationManager,
+ userContextProvider
+ )
+ }
+
+ @Test
+ fun startsTracing_afterReceivingActionStartCommand() {
+ underTest.handleStartCommand()
+ bgExecutor.runAllReady()
+
+ Truth.assertThat(issueRecordingState.isRecording).isTrue()
+ verify(traceurMessageSender).startTracing(any<TraceConfig>())
+ }
+
+ @Test
+ fun stopsTracing_afterReceivingStopTracingCommand() {
+ underTest.handleStopCommand(mContext.contentResolver)
+ bgExecutor.runAllReady()
+
+ Truth.assertThat(issueRecordingState.isRecording).isFalse()
+ verify(traceurMessageSender).stopTracing()
+ }
+
+ @Test
+ fun cancelsNotification_afterReceivingShareCommand() {
+ underTest.handleShareCommand(0, null, mContext)
+ bgExecutor.runAllReady()
+
+ verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>())
+ }
+
+ @Test
+ fun requestBugreport_afterReceivingShareCommand_withTakeBugreportTrue() {
+ issueRecordingState.takeBugreport = true
+ val uri = mock<Uri>()
+
+ underTest.handleShareCommand(0, uri, mContext)
+ bgExecutor.runAllReady()
+
+ verify(iActivityManager).requestBugReportWithExtraAttachment(uri)
+ }
+
+ @Test
+ fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() {
+ issueRecordingState.takeBugreport = false
+ val uri = mock<Uri>()
+
+ underTest.handleShareCommand(0, uri, mContext)
+ bgExecutor.runAllReady()
+
+ verify(traceurMessageSender).shareTraces(mContext, uri)
+ }
+
+ @Test
+ fun closesShade_afterReceivingShareCommand() {
+ val uri = mock<Uri>()
+
+ underTest.handleShareCommand(0, uri, mContext)
+ bgExecutor.runAllReady()
+
+ verify(panelInteractor).collapsePanels()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
index 263b001..78764c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
@@ -79,21 +79,6 @@
@Test
fun callsCallbackAndUpdatesProfilesWhenAnIntentReceived() = runTest {
- tracker =
- UserTrackerImpl(
- context,
- { fakeFeatures },
- userManager,
- iActivityManager,
- dumpManager,
- this,
- testDispatcher,
- handler
- )
- tracker.initialize(0)
- tracker.addCallback(callback, executor)
- val profileID = tracker.userId + 10
-
`when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
val id = invocation.getArgument<Int>(0)
val info = UserInfo(id, "", UserInfo.FLAG_FULL)
@@ -109,6 +94,21 @@
listOf(info, infoProfile)
}
+ tracker =
+ UserTrackerImpl(
+ context,
+ { fakeFeatures },
+ userManager,
+ iActivityManager,
+ dumpManager,
+ this,
+ testDispatcher,
+ handler
+ )
+ tracker.initialize(0)
+ tracker.addCallback(callback, executor)
+ val profileID = tracker.userId + 10
+
tracker.onReceive(context, Intent(intentAction))
verify(callback, times(0)).onUserChanged(anyInt(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 01a3d36..1d74331 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1112,9 +1112,11 @@
public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
+ // Returning false means unable to show the bouncer
when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
.thenReturn(KeyguardState.LOCKSCREEN);
+ mStatusBarKeyguardViewManager.onStartedWakingUp();
reset(mCentralSurfaces);
// Advance past reattempts
@@ -1127,6 +1129,23 @@
@Test
@DisableSceneContainer
+ @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
+ public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() {
+ when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
+ .thenReturn(KeyguardState.LOCKSCREEN);
+ mStatusBarKeyguardViewManager.onStartedGoingToSleep();
+
+ reset(mCentralSurfaces);
+ reset(mPrimaryBouncerInteractor);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
+ /* hideBouncerWhenShowing= */true, false);
+ verify(mCentralSurfaces).showKeyguard();
+ verify(mPrimaryBouncerInteractor).hide();
+ }
+
+
+ @Test
+ @DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
boolean isFalsingReset = false;
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
index 2a0e764..a639463 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
@@ -16,25 +16,31 @@
package com.android.systemui.accessibility.data.repository
+import com.android.systemui.accessibility.data.model.CaptioningModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeCaptioningRepository : CaptioningRepository {
- private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false)
- override val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
- get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow()
-
- private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false)
- override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
- get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow()
+ private val mutableCaptioningModel = MutableStateFlow<CaptioningModel?>(null)
+ override val captioningModel: StateFlow<CaptioningModel?> = mutableCaptioningModel.asStateFlow()
override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
- mutableIsSystemAudioCaptioningEnabled.value = isEnabled
+ mutableCaptioningModel.value =
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled = isEnabled,
+ isSystemAudioCaptioningUiEnabled =
+ mutableCaptioningModel.value?.isSystemAudioCaptioningUiEnabled == true,
+ )
}
- fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) {
- mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled
+ fun setIsSystemAudioCaptioningUiEnabled(isEnabled: Boolean) {
+ mutableCaptioningModel.value =
+ CaptioningModel(
+ isSystemAudioCaptioningEnabled =
+ mutableCaptioningModel.value?.isSystemAudioCaptioningEnabled == true,
+ isSystemAudioCaptioningUiEnabled = isEnabled,
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
index a5690a0..cb7750f5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
@@ -24,7 +24,6 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
val Kosmos.mediaDataLoader by
Kosmos.Fixture {
@@ -32,7 +31,6 @@
testableContext,
testDispatcher,
testScope,
- activityStarter,
fakeMediaControllerFactory,
mediaFlags,
imageLoader,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
index 632436a..174e653 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -26,6 +26,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.util.fakeMediaControllerFactory
import com.android.systemui.media.controls.util.mediaFlags
@@ -60,5 +61,6 @@
keyguardUpdateMonitor = keyguardUpdateMonitor,
mediaDataRepository = mediaDataRepository,
mediaDataLoader = { mediaDataLoader },
+ mediaLogger = mediaLogger,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
index b7660e0..b33edf9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
@@ -28,6 +28,8 @@
Kosmos.Fixture {
MediaTimeoutListener(
mediaControllerFactory = fakeMediaControllerFactory,
+ bgExecutor = fakeExecutor,
+ uiExecutor = fakeExecutor,
mainExecutor = fakeExecutor,
logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")),
statusBarStateController = statusBarStateController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index a0fc76b..4978558 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -24,6 +24,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
Kosmos.Fixture {
@@ -39,6 +40,7 @@
activityManager,
mock(),
fakeExecutor,
+ fakeSystemClock,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 55f3ed7..8744638 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -12,6 +12,8 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
var Kosmos.sceneKeys by Fixture {
@@ -70,6 +72,8 @@
sceneInteractor = sceneInteractor,
falsingInteractor = falsingInteractor,
powerInteractor = powerInteractor,
+ shadeInteractor = shadeInteractor,
+ splitEdgeDetector = splitEdgeDetector,
motionEventHandlerReceiver = {},
logger = sceneLogger
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
new file mode 100644
index 0000000..e0b5292
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.ui.unit.dp
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+var Kosmos.splitEdgeDetector: SplitEdgeDetector by
+ Kosmos.Fixture {
+ SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ edgeSize = 40.dp,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
new file mode 100644
index 0000000..78763f9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.utils
+
+import android.os.UserHandle
+
+class FakeUserScopedService<T>(private val defaultImplementation: T) : UserScopedService<T> {
+
+ private val implementations = mutableMapOf<UserHandle, T>()
+
+ fun addImplementation(user: UserHandle, implementation: T) {
+ implementations[user] = implementation
+ }
+
+ fun removeImplementation(user: UserHandle): T? = implementations.remove(user)
+
+ override fun forUser(user: UserHandle): T =
+ implementations.getOrDefault(user, defaultImplementation)
+}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
similarity index 82%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
index 4b9cf85..b582ccf 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -15,7 +15,7 @@
*/
package android.ravenwood.annotation;
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -29,8 +29,7 @@
*
* @hide
*/
-@Target({TYPE})
+@Target({METHOD})
@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
- String value();
+public @interface RavenwoodRedirect {
}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
similarity index 94%
rename from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
index 4b9cf85..bee9222 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
@@ -31,6 +31,6 @@
*/
@Target({TYPE})
@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
+public @interface RavenwoodRedirectionClass {
String value();
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
rename to ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
index f38d565..e21a9cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.database;
-import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Parcel;
import android.util.Base64;
@@ -35,8 +34,8 @@
private String mName;
private int mColumnNum;
private static class Row {
- String[] fields;
- int[] types;
+ String[] mFields;
+ int[] mTypes;
}
private final List<Row> mRows = new ArrayList<>();
@@ -69,9 +68,9 @@
public static boolean nativeAllocRow(long windowPtr) {
CursorWindow_host instance = sInstances.get(windowPtr);
Row row = new Row();
- row.fields = new String[instance.mColumnNum];
- row.types = new int[instance.mColumnNum];
- Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL);
+ row.mFields = new String[instance.mColumnNum];
+ row.mTypes = new int[instance.mColumnNum];
+ Arrays.fill(row.mTypes, Cursor.FIELD_TYPE_NULL);
instance.mRows.add(row);
return true;
}
@@ -82,8 +81,8 @@
return false;
}
Row r = instance.mRows.get(row);
- r.fields[column] = value;
- r.types[column] = type;
+ r.mFields[column] = value;
+ r.mTypes[column] = type;
return true;
}
@@ -93,7 +92,7 @@
return Cursor.FIELD_TYPE_NULL;
}
- return instance.mRows.get(row).types[column];
+ return instance.mRows.get(row).mTypes[column];
}
public static boolean nativePutString(long windowPtr, String value,
@@ -107,7 +106,7 @@
return null;
}
- return instance.mRows.get(row).fields[column];
+ return instance.mRows.get(row).mFields[column];
}
public static boolean nativePutLong(long windowPtr, long value, int row, int column) {
@@ -170,8 +169,8 @@
parcel.writeInt(window.mColumnNum);
parcel.writeInt(window.mRows.size());
for (int row = 0; row < window.mRows.size(); row++) {
- parcel.writeStringArray(window.mRows.get(row).fields);
- parcel.writeIntArray(window.mRows.get(row).types);
+ parcel.writeStringArray(window.mRows.get(row).mFields);
+ parcel.writeIntArray(window.mRows.get(row).mTypes);
}
}
@@ -183,8 +182,8 @@
int rowCount = parcel.readInt();
for (int row = 0; row < rowCount; row++) {
Row r = new Row();
- r.fields = parcel.createStringArray();
- r.types = parcel.createIntArray();
+ r.mFields = parcel.createStringArray();
+ r.mTypes = parcel.createIntArray();
window.mRows.add(r);
}
return windowPtr;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
similarity index 97%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
index 5e81124..1b63adc 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
similarity index 99%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
index cb00b3e..720f1d2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
import android.system.ErrnoException;
import android.system.Os;
@@ -527,4 +527,4 @@
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
index e7479d3..b09bf31 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
import android.util.SparseArray;
@@ -36,9 +36,6 @@
/** Predicate tested to determine if a given key can be written. */
@GuardedBy("sLock")
private static Predicate<String> sKeyWritablePredicate;
- /** Callback to trigger when values are changed */
- @GuardedBy("sLock")
- private static Runnable sChangeCallback;
/**
* Reverse mapping that provides a way back to an original key from the
@@ -48,7 +45,7 @@
private static SparseArray<String> sKeyHandles = new SparseArray<>();
/**
- * Basically the same as {@link #native_init$ravenwood}, but it'll only run if no values are
+ * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are
* set yet.
*/
public static void initializeIfNeeded(Map<String, String> values,
@@ -57,30 +54,32 @@
if (sValues != null) {
return; // Already initialized.
}
- native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
- () -> {});
+ init$ravenwood(values, keyReadablePredicate, keyWritablePredicate);
}
}
- public static void native_init$ravenwood(Map<String, String> values,
- Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
- Runnable changeCallback) {
+ public static void init$ravenwood(Map<String, String> values,
+ Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
synchronized (sLock) {
sValues = Objects.requireNonNull(values);
sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
- sChangeCallback = Objects.requireNonNull(changeCallback);
sKeyHandles.clear();
+ synchronized (SystemProperties.sChangeCallbacks) {
+ SystemProperties.sChangeCallbacks.clear();
+ }
}
}
- public static void native_reset$ravenwood() {
+ public static void reset$ravenwood() {
synchronized (sLock) {
sValues = null;
sKeyReadablePredicate = null;
sKeyWritablePredicate = null;
- sChangeCallback = null;
sKeyHandles.clear();
+ synchronized (SystemProperties.sChangeCallbacks) {
+ SystemProperties.sChangeCallbacks.clear();
+ }
}
}
@@ -101,7 +100,7 @@
} else {
sValues.put(key, val);
}
- sChangeCallback.run();
+ SystemProperties.callChangeCallbacks();
}
}
@@ -183,7 +182,7 @@
// Report through callback always registered via init above
synchronized (sLock) {
Preconditions.requireNonNullViaRavenwoodRule(sValues);
- sChangeCallback.run();
+ SystemProperties.callChangeCallbacks();
}
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
similarity index 83%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
index 55d4ffb..878a0ff 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
import com.android.internal.os.RuntimeInit;
import java.io.PrintStream;
-import java.util.Collection;
public class EventLog_host {
public static int writeEvent(int tag, int value) {
@@ -58,15 +57,6 @@
return sb.length();
}
- public static void readEvents(int[] tags, Collection<android.util.EventLog.Event> output) {
- throw new UnsupportedOperationException();
- }
-
- public static void readEventsOnWrapping(int[] tags, long timestamp,
- Collection<android.util.EventLog.Event> output) {
- throw new UnsupportedOperationException();
- }
-
/**
* Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
* that we don't end up in a recursive loop.
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
similarity index 95%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/Log_host.java
index f301b9c..d232ef2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
-import android.util.Log;
import android.util.Log.Level;
import com.android.internal.os.RuntimeInit;
@@ -44,7 +43,7 @@
case Log.LOG_ID_SYSTEM: buffer = "system"; break;
case Log.LOG_ID_CRASH: buffer = "crash"; break;
default: buffer = "buf:" + bufID; break;
- };
+ }
final String prio;
switch (priority) {
@@ -55,7 +54,7 @@
case Log.ERROR: prio = "E"; break;
case Log.ASSERT: prio = "A"; break;
default: prio = "prio:" + priority; break;
- };
+ }
for (String s : msg.split("\\n")) {
getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
new file mode 100644
index 0000000..c18c307
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
@@ -0,0 +1,63 @@
+/*
+ * 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.internal.os;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class LongArrayContainer_host {
+ private static final HashMap<Long, long[]> sInstances = new HashMap<>();
+ private static long sNextId = 1;
+
+ public static long native_init(int arrayLength) {
+ long[] array = new long[arrayLength];
+ long instanceId = sNextId++;
+ sInstances.put(instanceId, array);
+ return instanceId;
+ }
+
+ static long[] getInstance(long instanceId) {
+ return sInstances.get(instanceId);
+ }
+
+ public static void native_setValues(long instanceId, long[] values) {
+ System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
+ }
+
+ public static void native_getValues(long instanceId, long[] values) {
+ System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
+ }
+
+ public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
+ long[] values = getInstance(instanceId);
+
+ boolean nonZero = false;
+ Arrays.fill(array, 0);
+
+ for (int i = 0; i < values.length; i++) {
+ int index = indexMap[i];
+ if (index < 0 || index >= array.length) {
+ throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
+ + (array.length - 1) + "]");
+ }
+ if (values[i] != 0) {
+ array[index] += values[i];
+ nonZero = true;
+ }
+ }
+ return nonZero;
+ }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
similarity index 87%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
index 0f65544..9ce8ea8 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
import android.os.BadParcelableException;
import android.os.Parcel;
@@ -28,7 +28,7 @@
public class LongArrayMultiStateCounter_host {
/**
- * A reimplementation of {@link com.android.internal.os.LongArrayMultiStateCounter}, only in
+ * A reimplementation of {@link LongArrayMultiStateCounter}, only in
* Java instead of native. The majority of the code (in C++) can be found in
* /frameworks/native/libs/battery/MultiStateCounter.h
*/
@@ -257,50 +257,6 @@
}
}
- public static class LongArrayContainer_host {
- private static final HashMap<Long, long[]> sInstances = new HashMap<>();
- private static long sNextId = 1;
-
- public static long native_init(int arrayLength) {
- long[] array = new long[arrayLength];
- long instanceId = sNextId++;
- sInstances.put(instanceId, array);
- return instanceId;
- }
-
- static long[] getInstance(long instanceId) {
- return sInstances.get(instanceId);
- }
-
- public static void native_setValues(long instanceId, long[] values) {
- System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
- }
-
- public static void native_getValues(long instanceId, long[] values) {
- System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
- }
-
- public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
- long[] values = getInstance(instanceId);
-
- boolean nonZero = false;
- Arrays.fill(array, 0);
-
- for (int i = 0; i < values.length; i++) {
- int index = indexMap[i];
- if (index < 0 || index >= array.length) {
- throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
- + (array.length - 1) + "]");
- }
- if (values[i] != 0) {
- array[index] += values[i];
- nonZero = true;
- }
- }
- return nonZero;
- }
- }
-
private static final HashMap<Long, LongArrayMultiStateCounterRavenwood> sInstances =
new HashMap<>();
private static long sNextId = 1;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
similarity index 98%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
index 9486651..1d95aa1 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
import android.os.BadParcelableException;
import android.os.Parcel;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
similarity index 86%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
index 58f6bbb..3bf116d 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.ravenwood;
+import android.os.SystemProperties_host;
import android.platform.test.ravenwood.RavenwoodSystemProperties;
import android.util.Log;
-import com.android.internal.ravenwood.RavenwoodEnvironment;
import com.android.ravenwood.common.JvmWorkaround;
import com.android.ravenwood.common.RavenwoodCommonUtils;
@@ -36,7 +36,7 @@
/**
* Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
*/
- public static void nativeEnsureRavenwoodInitialized() {
+ public static void ensureRavenwoodInitialized() {
// TODO Unify it with the initialization code in RavenwoodAwareTestRunnerHook.
@@ -63,14 +63,14 @@
/**
* Called from {@link RavenwoodEnvironment#getRavenwoodRuntimePath()}.
*/
- public static String nativeGetRavenwoodRuntimePath(RavenwoodEnvironment env) {
+ public static String getRavenwoodRuntimePath(RavenwoodEnvironment env) {
return RavenwoodCommonUtils.getRavenwoodRuntimePath();
}
/**
* Called from {@link RavenwoodEnvironment#fromAddress(long)}.
*/
- public static <T> T nativeFromAddress(RavenwoodEnvironment env, long address) {
+ public static <T> T fromAddress(RavenwoodEnvironment env, long address) {
return JvmWorkaround.getInstance().fromAddress(address);
}
}
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 952ab82..3ec3e3c 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -32,8 +32,11 @@
--substitute-annotation
android.ravenwood.annotation.RavenwoodReplace
---native-substitute-annotation
- android.ravenwood.annotation.RavenwoodNativeSubstitutionClass
+--redirect-annotation
+ android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+ android.ravenwood.annotation.RavenwoodRedirectionClass
--class-load-hook-annotation
android.ravenwood.annotation.RavenwoodClassLoadHook
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index eba628d..0947238 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -16,15 +16,15 @@
package com.android.server.appfunctions;
-import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
-
-import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchManager.SearchContext;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
import android.app.appsearch.GetSchemaResponse;
import android.app.appsearch.PutDocumentsRequest;
import android.app.appsearch.SearchResult;
@@ -42,10 +42,7 @@
import java.util.Objects;
import java.util.concurrent.Executor;
-/**
- * A future API wrapper of {@link AppSearchSession} APIs.
- */
-@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+/** A future API wrapper of {@link AppSearchSession} APIs. */
public class FutureAppSearchSession implements Closeable {
private static final String TAG = FutureAppSearchSession.class.getSimpleName();
private final Executor mExecutor;
@@ -67,14 +64,14 @@
/** Converts a failed app search result codes into an exception. */
@NonNull
- private static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
+ public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
return switch (appSearchResult.getResultCode()) {
- case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
- appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_IO_ERROR -> new IOException(
- appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
- appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_INVALID_ARGUMENT ->
+ new IllegalArgumentException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_IO_ERROR ->
+ new IOException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_SECURITY_ERROR ->
+ new SecurityException(appSearchResult.getErrorMessage());
default -> new IllegalStateException(appSearchResult.getErrorMessage());
};
}
@@ -137,14 +134,16 @@
/** Indexes documents into the AppSearchSession database. */
public AndroidFuture<AppSearchBatchResult<String, Void>> put(
@NonNull PutDocumentsRequest putDocumentsRequest) {
- return getSessionAsync().thenCompose(
- session -> {
- AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
- new AndroidFuture<>();
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
+ new AndroidFuture<>();
- session.put(putDocumentsRequest, mExecutor, batchResultFuture::complete);
- return batchResultFuture;
- });
+ session.put(
+ putDocumentsRequest, mExecutor, batchResultFuture::complete);
+ return batchResultFuture;
+ });
}
/**
@@ -152,10 +151,9 @@
* of search provided.
*/
public AndroidFuture<FutureSearchResults> search(
- @NonNull String queryExpression,
- @NonNull SearchSpec searchSpec) {
- return getSessionAsync().thenApply(
- session -> session.search(queryExpression, searchSpec))
+ @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+ return getSessionAsync()
+ .thenApply(session -> session.search(queryExpression, searchSpec))
.thenApply(result -> new FutureSearchResults(result, mExecutor));
}
@@ -173,8 +171,8 @@
private final SearchResults mSearchResults;
private final Executor mExecutor;
- public FutureSearchResults(@NonNull SearchResults searchResults,
- @NonNull Executor executor) {
+ public FutureSearchResults(
+ @NonNull SearchResults searchResults, @NonNull Executor executor) {
mSearchResults = Objects.requireNonNull(searchResults);
mExecutor = Objects.requireNonNull(executor);
}
@@ -184,15 +182,68 @@
new AndroidFuture<>();
mSearchResults.getNextPage(mExecutor, nextPageFuture::complete);
- return nextPageFuture.thenApply(result -> {
- if (result.isSuccess()) {
- return result.getResultValue();
- } else {
- throw new RuntimeException(
- failedResultToException(result));
- }
- });
+ return nextPageFuture.thenApply(
+ result -> {
+ if (result.isSuccess()) {
+ return result.getResultValue();
+ } else {
+ throw new RuntimeException(failedResultToException(result));
+ }
+ });
+ }
+ }
+
+ /** A future API to retrieve a document by its id from the local AppSearch session. */
+ public AndroidFuture<GenericDocument> getByDocumentId(
+ @NonNull String documentId, @NonNull String namespace) {
+ Objects.requireNonNull(documentId);
+ Objects.requireNonNull(namespace);
+
+ GetByDocumentIdRequest request =
+ new GetByDocumentIdRequest.Builder(namespace)
+ .addIds(documentId)
+ .build();
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ AndroidFuture<AppSearchBatchResult<String, GenericDocument>>
+ batchResultFuture = new AndroidFuture<>();
+ session.getByDocumentId(
+ request,
+ mExecutor,
+ new BatchResultCallbackAdapter<>(batchResultFuture));
+
+ return batchResultFuture.thenApply(
+ batchResult ->
+ getGenericDocumentFromBatchResult(
+ batchResult, documentId));
+ });
+ }
+
+ private static GenericDocument getGenericDocumentFromBatchResult(
+ AppSearchBatchResult<String, GenericDocument> result, String documentId) {
+ if (result.isSuccess()) {
+ return result.getSuccesses().get(documentId);
+ }
+ throw new IllegalArgumentException("No document in the result for id: " + documentId);
+ }
+
+ private static final class BatchResultCallbackAdapter<K, V>
+ implements BatchResultCallback<K, V> {
+ private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
+
+ BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) {
+ mFuture = future;
}
+ @Override
+ public void onResult(@NonNull AppSearchBatchResult<K, V> result) {
+ mFuture.complete(result);
+ }
+
+ @Override
+ public void onSystemError(Throwable t) {
+ mFuture.completeExceptionally(t);
+ }
}
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
new file mode 100644
index 0000000..0c22624
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/** A wrapper around {@link GlobalSearchSession} that provides a future-based API. */
+public class FutureGlobalSearchSession implements Closeable {
+ private static final String TAG = FutureGlobalSearchSession.class.getSimpleName();
+ private final Executor mExecutor;
+ private final AndroidFuture<AppSearchResult<GlobalSearchSession>> mSettableSessionFuture;
+
+ public FutureGlobalSearchSession(
+ @NonNull AppSearchManager appSearchManager, @NonNull Executor executor) {
+ this.mExecutor = executor;
+ mSettableSessionFuture = new AndroidFuture<>();
+ appSearchManager.createGlobalSearchSession(mExecutor, mSettableSessionFuture::complete);
+ }
+
+ private AndroidFuture<GlobalSearchSession> getSessionAsync() {
+ return mSettableSessionFuture.thenApply(
+ result -> {
+ if (result.isSuccess()) {
+ return result.getResultValue();
+ } else {
+ throw new RuntimeException(
+ FutureAppSearchSession.failedResultToException(result));
+ }
+ });
+ }
+
+ /**
+ * Registers an observer callback for the given target package name.
+ *
+ * @param targetPackageName The package name of the target app.
+ * @param spec The observer spec.
+ * @param executor The executor to run the observer callback on.
+ * @param observer The observer callback to register.
+ * @return A future that completes once the observer is registered.
+ */
+ public AndroidFuture<Void> registerObserverCallbackAsync(
+ String targetPackageName,
+ ObserverSpec spec,
+ Executor executor,
+ ObserverCallback observer) {
+ return getSessionAsync()
+ .thenCompose(
+ session -> {
+ try {
+ session.registerObserverCallback(
+ targetPackageName, spec, executor, observer);
+ return AndroidFuture.completedFuture(null);
+ } catch (AppSearchException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ try {
+ getSessionAsync().get().close();
+ } catch (Exception ex) {
+ Slog.e(TAG, "Failed to close global search session", ex);
+ }
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
new file mode 100644
index 0000000..be5770b
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * This class implements helper methods for synchronously interacting with AppSearch while
+ * synchronizing AppFunction runtime and static metadata.
+ */
+public class MetadataSyncAdapter {
+ private final FutureAppSearchSession mFutureAppSearchSession;
+ private final Executor mSyncExecutor;
+
+ public MetadataSyncAdapter(
+ @NonNull Executor syncExecutor,
+ @NonNull FutureAppSearchSession futureAppSearchSession) {
+ mSyncExecutor = Objects.requireNonNull(syncExecutor);
+ mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession);
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids that are in the static
+ * metadata but not in the runtime metadata.
+ *
+ * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+ * static metadata.
+ * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+ * runtime metadata.
+ * @return A map of package names to a set of function ids that are in the static metadata but
+ * not in the runtime metadata.
+ */
+ @NonNull
+ @VisibleForTesting
+ static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+ ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+ return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids that are in the runtime
+ * metadata but not in the static metadata.
+ *
+ * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+ * static metadata.
+ * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+ * runtime metadata.
+ * @return A map of package names to a set of function ids that are in the runtime metadata but
+ * not in the static metadata.
+ */
+ @NonNull
+ @VisibleForTesting
+ static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+ ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+ return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap);
+ }
+
+ @NonNull
+ private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap(
+ ArrayMap<String, ArraySet<String>> packageToFunctionMapA,
+ ArrayMap<String, ArraySet<String>> packageToFunctionMapB) {
+ ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>();
+ for (String packageName : packageToFunctionMapA.keySet()) {
+ if (!packageToFunctionMapB.containsKey(packageName)) {
+ diffMap.put(packageName, packageToFunctionMapA.get(packageName));
+ continue;
+ }
+ ArraySet<String> diffFunctions = new ArraySet<>();
+ for (String functionId :
+ Objects.requireNonNull(packageToFunctionMapA.get(packageName))) {
+ if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName))
+ .contains(functionId)) {
+ diffFunctions.add(functionId);
+ }
+ }
+ if (!diffFunctions.isEmpty()) {
+ diffMap.put(packageName, diffFunctions);
+ }
+ }
+ return diffMap;
+ }
+
+ /**
+ * This method returns a map of package names to a set of function ids.
+ *
+ * @param queryExpression The query expression to use when searching for AppFunction metadata.
+ * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata.
+ * @return A map of package names to a set of function ids.
+ * @throws ExecutionException If the future search results fail to execute.
+ * @throws InterruptedException If the future search results are interrupted.
+ */
+ @NonNull
+ @VisibleForTesting
+ @WorkerThread
+ ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
+ @NonNull String queryExpression,
+ @NonNull SearchSpec metadataSearchSpec,
+ @NonNull String propertyFunctionId,
+ @NonNull String propertyPackageName)
+ throws ExecutionException, InterruptedException {
+ ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
+ FutureSearchResults futureSearchResults =
+ mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get();
+ List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
+ // TODO(b/357551503): This could be expensive if we have more functions
+ while (!searchResultsList.isEmpty()) {
+ for (SearchResult searchResult : searchResultsList) {
+ String packageName =
+ searchResult.getGenericDocument().getPropertyString(propertyPackageName);
+ String functionId =
+ searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
+ packageToFunctionIds
+ .computeIfAbsent(packageName, k -> new ArraySet<>())
+ .add(functionId);
+ }
+ searchResultsList = futureSearchResults.getNextPage().get();
+ }
+ return packageToFunctionIds;
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
index 98903ae..58597c3 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
@@ -25,7 +25,6 @@
* services are properly unbound after the operation completes or a timeout occurs.
*
* @param <T> Class of wrapped service.
- * @hide
*/
public interface RemoteServiceCaller<T> {
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
index 0e18705..eea17ee 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
@@ -34,7 +34,6 @@
* Context#bindService}.
*
* @param <T> Class of wrapped service.
- * @hide
*/
public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> {
private static final String TAG = "AppFunctionsServiceCall";
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 2937307..99c3eca 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -139,6 +139,7 @@
static final String[] sDeviceConfigAconfigScopes = new String[] {
"accessibility",
"android_core_networking",
+ "android_health_services",
"android_sdk",
"android_stylus",
"aoc",
@@ -235,7 +236,6 @@
"wear_connectivity",
"wear_esim_carriers",
"wear_frameworks",
- "wear_health_services",
"wear_media",
"wear_offload",
"wear_security",
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 780eda6..6daf0d0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -12712,11 +12712,6 @@
if (mController == null)
return;
try {
- // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO
- if (isStreamBluetoothSco(streamType)) {
- // TODO: notify both sco and voice_call about volume changes
- streamType = AudioSystem.STREAM_BLUETOOTH_SCO;
- }
mController.volumeChanged(streamType, flags);
} catch (RemoteException e) {
Log.w(TAG, "Error calling volumeChanged", e);
@@ -14727,6 +14722,7 @@
@Override
/** @see AudioManager#permissionUpdateBarrier() */
public void permissionUpdateBarrier() {
+ if (!audioserverPermissions()) return;
mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle);
List<Future> snapshot;
synchronized (mScheduledPermissionTasks) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 4b85217..101596d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1150,6 +1150,13 @@
return Constants.ABORT_REFUSED;
}
+ if (mArcEstablished) {
+ HdmiLogger.debug("ARC is already established.");
+ HdmiCecMessage command = HdmiCecMessageBuilder.buildReportArcInitiated(
+ getDeviceInfo().getLogicalAddress(), message.getSource());
+ mService.sendCecCommand(command);
+ return Constants.HANDLED;
+ }
// In case where <Initiate Arc> is started by <Request ARC Initiation>, this message is
// handled in RequestArcInitiationAction as well.
SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
diff --git a/services/core/java/com/android/server/hdmi/RequestSadAction.java b/services/core/java/com/android/server/hdmi/RequestSadAction.java
index 0188e96..2566300 100644
--- a/services/core/java/com/android/server/hdmi/RequestSadAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestSadAction.java
@@ -19,6 +19,8 @@
import android.hardware.hdmi.HdmiControlManager;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -36,7 +38,8 @@
// State in which the action is waiting for <Report Short Audio Descriptor>.
private static final int STATE_WAITING_FOR_REPORT_SAD = 1;
private static final int MAX_SAD_PER_REQUEST = 4;
- private static final int RETRY_COUNTER_MAX = 1;
+ @VisibleForTesting
+ public static final int RETRY_COUNTER_MAX = 3;
private final int mTargetAddress;
private final RequestSadCallback mCallback;
private final List<Integer> mCecCodecsToQuery = new ArrayList<>();
diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
index 5ab22e1..e6abcb9 100644
--- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
+++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
@@ -60,6 +60,12 @@
boolean start() {
// Seq #37.
if (mEnabled) {
+ // Avoid triggering duplicate RequestSadAction events.
+ // This could lead to unexpected responses from the AVR and cause the TV to receive data
+ // out of order. The SAD report does not provide information about the order of events.
+ if ((tv().hasAction(RequestSadAction.class))) {
+ return true;
+ }
// Request SADs before enabling ARC
RequestSadAction action = new RequestSadAction(
localDevice(), Constants.ADDR_AUDIO_SYSTEM,
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 84cee7e..1285a61 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2269,13 +2269,15 @@
// Native callback.
@SuppressWarnings("unused")
private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) {
- // TODO(b/286551975): sent the touchpad hardware state data here to TouchpadDebugActivity
Slog.d(TAG, "notifyTouchpadHardwareState: Time: "
+ hardwareStates.getTimestamp() + ", No. Buttons: "
+ hardwareStates.getButtonsDown() + ", No. Fingers: "
+ hardwareStates.getFingerCount() + ", No. Touch: "
+ hardwareStates.getTouchCount() + ", Id: "
+ deviceId);
+ if (mTouchpadDebugViewController != null) {
+ mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates);
+ }
}
// Native callback.
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 7785ffb..ba56ad0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -30,6 +30,9 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
+
import java.util.Objects;
public class TouchpadDebugView extends LinearLayout {
@@ -52,6 +55,10 @@
private int mScreenHeight;
private int mWindowLocationBeforeDragX;
private int mWindowLocationBeforeDragY;
+ @NonNull
+ private TouchpadHardwareState mLastTouchpadState =
+ new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]);
public TouchpadDebugView(Context context, int touchpadId) {
super(context);
@@ -83,14 +90,14 @@
private void init(Context context) {
setOrientation(VERTICAL);
- setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.WRAP_CONTENT));
- setBackgroundColor(Color.TRANSPARENT);
+ setLayoutParams(new LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ setBackgroundColor(Color.RED);
// TODO(b/286551975): Replace this content with the touchpad debug view.
TextView textView1 = new TextView(context);
- textView1.setBackgroundColor(Color.parseColor("#FFFF0000"));
+ textView1.setBackgroundColor(Color.TRANSPARENT);
textView1.setTextSize(20);
textView1.setText("Touchpad Debug View 1");
textView1.setGravity(Gravity.CENTER);
@@ -98,7 +105,7 @@
textView1.setLayoutParams(new LayoutParams(1000, 200));
TextView textView2 = new TextView(context);
- textView2.setBackgroundColor(Color.BLUE);
+ textView2.setBackgroundColor(Color.TRANSPARENT);
textView2.setTextSize(20);
textView2.setText("Touchpad Debug View 2");
textView2.setGravity(Gravity.CENTER);
@@ -126,9 +133,7 @@
case MotionEvent.ACTION_MOVE:
deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX;
deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY;
- Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop);
if (isSlopExceeded(deltaX, deltaY)) {
- Slog.d("TouchpadDebugView", "Slop exceeded");
mWindowLayoutParams.x =
Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX),
mScreenWidth - this.getWidth()));
@@ -136,9 +141,6 @@
Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY),
mScreenHeight - this.getHeight()));
- Slog.d("TouchpadDebugView", "New position X: "
- + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y);
-
mWindowManager.updateViewLayout(this, mWindowLayoutParams);
}
return true;
@@ -166,7 +168,7 @@
@Override
public boolean performClick() {
super.performClick();
- Slog.d("TouchpadDebugView", "You clicked me!");
+ Slog.d("TouchpadDebugView", "You tapped the window!");
return true;
}
@@ -201,4 +203,34 @@
public WindowManager.LayoutParams getWindowLayoutParams() {
return mWindowLayoutParams;
}
+
+ public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) {
+ if (mLastTouchpadState.getButtonsDown() == 0) {
+ if (touchpadHardwareState.getButtonsDown() > 0) {
+ onTouchpadButtonPress();
+ }
+ } else {
+ if (touchpadHardwareState.getButtonsDown() == 0) {
+ onTouchpadButtonRelease();
+ }
+ }
+ mLastTouchpadState = touchpadHardwareState;
+ }
+
+ private void onTouchpadButtonPress() {
+ Slog.d("TouchpadDebugView", "You clicked me!");
+
+ // Iterate through all child views
+ // Temporary demonstration for testing
+ for (int i = 0; i < getChildCount(); i++) {
+ getChildAt(i).setBackgroundColor(Color.BLUE);
+ }
+ }
+
+ private void onTouchpadButtonRelease() {
+ Slog.d("TouchpadDebugView", "You released the click");
+ for (int i = 0; i < getChildCount(); i++) {
+ getChildAt(i).setBackgroundColor(Color.RED);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index c28e74a..bc53c49 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -27,6 +27,7 @@
import com.android.server.input.InputManagerService;
import com.android.server.input.TouchpadHardwareProperties;
+import com.android.server.input.TouchpadHardwareState;
import java.util.Objects;
@@ -132,4 +133,10 @@
mTouchpadDebugView = null;
Slog.d(TAG, "Touchpad debug view removed.");
}
+
+ public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) {
+ if (mTouchpadDebugView != null) {
+ mTouchpadDebugView.updateHardwareState(touchpadHardwareState);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 4fcf27d..38ef5b8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -3403,8 +3403,13 @@
// It's OK to dump the credential type since anyone with physical access can just
// observe it from the keyguard directly.
pw.println("Quality: " + getKeyguardStoredQuality(userId));
- pw.println("CredentialType: " + LockPatternUtils.credentialTypeToString(
- getCredentialTypeInternal(userId)));
+ final int credentialType = getCredentialTypeInternal(userId);
+ pw.println("CredentialType: "
+ + LockPatternUtils.credentialTypeToString(credentialType));
+ if (credentialType == CREDENTIAL_TYPE_NONE) {
+ pw.println("IsLockScreenDisabled: "
+ + getBoolean(LockPatternUtils.DISABLE_LOCKSCREEN_KEY, false, userId));
+ }
pw.println("SeparateChallenge: " + getSeparateProfileChallengeEnabledInternal(userId));
pw.println(TextUtils.formatSimple("Metrics: %s",
getUserPasswordMetrics(userId) != null ? "known" : "unknown"));
diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
index bad959a..925ba17 100644
--- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
+++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
@@ -22,6 +22,7 @@
import static com.android.server.notification.ZenLog.traceApplyDeviceEffect;
import static com.android.server.notification.ZenLog.traceScheduleApplyDeviceEffect;
+import android.app.KeyguardManager;
import android.app.UiModeManager;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@
private final Context mContext;
private final ColorDisplayManager mColorDisplayManager;
+ private final KeyguardManager mKeyguardManager;
private final PowerManager mPowerManager;
private final UiModeManager mUiModeManager;
private final WallpaperManager mWallpaperManager;
@@ -67,6 +69,7 @@
DefaultDeviceEffectsApplier(Context context) {
mContext = context;
mColorDisplayManager = context.getSystemService(ColorDisplayManager.class);
+ mKeyguardManager = context.getSystemService(KeyguardManager.class);
mPowerManager = context.getSystemService(PowerManager.class);
mUiModeManager = context.getSystemService(UiModeManager.class);
WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class);
@@ -133,12 +136,14 @@
// Changing the theme can be disruptive for the user (Activities are likely recreated, may
// lose some state). Therefore we only apply the change immediately if the rule was
- // activated manually, or we are initializing, or the screen is currently off/dreaming.
+ // activated manually, or we are initializing, or the screen is currently off/dreaming,
+ // or if the device is locked.
if (origin == ZenModeConfig.ORIGIN_INIT
|| origin == ZenModeConfig.ORIGIN_INIT_USER
|| origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
|| origin == ZenModeConfig.ORIGIN_USER_IN_APP
- || !mPowerManager.isInteractive()) {
+ || !mPowerManager.isInteractive()
+ || (android.app.Flags.modesUi() && mKeyguardManager.isKeyguardLocked())) {
unregisterScreenOffReceiver();
updateNightModeImmediately(useNightMode);
} else {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 43a285c..2856eb4 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1019,7 +1019,9 @@
&& scanInstallPackages(requests, createdAppId, versionInfos)) {
List<ReconciledPackage> reconciledPackages =
reconcileInstallPackages(requests, versionInfos);
- if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) {
+ if (reconciledPackages != null
+ && renameAndUpdatePaths(requests)
+ && commitInstallPackages(reconciledPackages)) {
success = true;
}
}
@@ -1029,24 +1031,49 @@
}
}
- private boolean prepareInstallPackages(List<InstallRequest> requests) {
- // TODO: will remove the locking after doRename is moved out of prepare
+ private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
for (InstallRequest request : requests) {
- try {
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
- request.onPrepareStarted();
- preparePackageLI(request);
- } catch (PrepareFailure prepareFailure) {
- request.setError(prepareFailure.error,
- prepareFailure.getMessage());
- request.setOriginPackage(prepareFailure.mConflictingPackage);
- request.setOriginPermission(prepareFailure.mConflictingPermission);
- return false;
- } finally {
- request.onPrepareFinished();
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ ParsedPackage parsedPackage = request.getParsedPackage();
+ final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
+ if (isApex) {
+ continue;
}
+ try {
+ doRenameLI(request, parsedPackage);
+ setUpFsVerity(parsedPackage);
+ } catch (Installer.InstallerException | IOException | DigestException
+ | NoSuchAlgorithmException | PrepareFailure e) {
+ request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
+ "Failed to set up verity: " + e);
+ return false;
+ }
+
+ // update paths that are set before renaming
+ PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
+ scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
+ scannedPackageSetting.setLegacyNativeLibraryPath(
+ parsedPackage.getNativeLibraryRootDir());
+ }
+ return true;
+ }
+ }
+
+ private boolean prepareInstallPackages(List<InstallRequest> requests) {
+ for (InstallRequest request : requests) {
+ try {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
+ request.onPrepareStarted();
+ preparePackage(request);
+ } catch (PrepareFailure prepareFailure) {
+ request.setError(prepareFailure.error,
+ prepareFailure.getMessage());
+ request.setOriginPackage(prepareFailure.mConflictingPackage);
+ request.setOriginPermission(prepareFailure.mConflictingPermission);
+ return false;
+ } finally {
+ request.onPrepareFinished();
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
return true;
@@ -1231,8 +1258,7 @@
return newProp != null && newProp.getBoolean();
}
- @GuardedBy("mPm.mInstallLock")
- private void preparePackageLI(InstallRequest request) throws PrepareFailure {
+ private void preparePackage(InstallRequest request) throws PrepareFailure {
final int[] allUsers = mPm.mUserManager.getUserIds();
final int installFlags = request.getInstallFlags();
final boolean onExternal = request.getVolumeUuid() != null;
@@ -1739,18 +1765,7 @@
}
}
- if (!isApex) {
- doRenameLI(request, parsedPackage);
-
- try {
- setUpFsVerity(parsedPackage);
- } catch (Installer.InstallerException | IOException | DigestException
- | NoSuchAlgorithmException e) {
- throw PrepareFailure.ofInternalError(
- "Failed to set up verity: " + e,
- PackageManagerException.INTERNAL_ERROR_VERITY_SETUP);
- }
- } else {
+ if (isApex) {
// Use the path returned by apexd
parsedPackage.setPath(request.getApexInfo().modulePath);
parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
@@ -1882,10 +1897,16 @@
}
if (!oldSharedUid.equals(newSharedUid)) {
- throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
- "Package " + parsedPackage.getPackageName()
- + " shared user changed from "
- + oldSharedUid + " to " + newSharedUid);
+ if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null
+ && ps.isArchivedOnAnyUser(allUsers))) {
+ // Only allow changing sharedUserId if unarchiving
+ // TODO(b/361558423): remove this check after pre-archiving installs
+ // accept a sharedUserId param in the API
+ throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+ "Package " + parsedPackage.getPackageName()
+ + " shared user changed from "
+ + oldSharedUid + " to " + newSharedUid);
+ }
}
// APK should not re-join shared UID
@@ -2086,7 +2107,21 @@
// Reflect the rename in scanned details
try {
- parsedPackage.setPath(afterCodeFile.getCanonicalPath());
+ String afterCanonicalPath = afterCodeFile.getCanonicalPath();
+ String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
+ parsedPackage.setPath(afterCanonicalPath);
+
+ parsedPackage.setNativeLibraryDir(
+ parsedPackage.getNativeLibraryDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ parsedPackage.setNativeLibraryRootDir(
+ parsedPackage.getNativeLibraryRootDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
+ if (secondaryNativeLibraryDir != null) {
+ parsedPackage.setSecondaryNativeLibraryDir(
+ secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
+ }
} catch (IOException e) {
Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);
throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9fb9e71..9428de7 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -925,6 +925,18 @@
return PackageArchiver.isArchived(readUserState(userId));
}
+ /**
+ * @return if the package is archived in any of the users
+ */
+ boolean isArchivedOnAnyUser(int[] userIds) {
+ for (int user : userIds) {
+ if (isArchived(user)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
int getInstallReason(int userId) {
return readUserState(userId).getInstallReason();
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fcd7f1..ed9dcfa 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -535,7 +535,7 @@
volatile boolean mRequestedOrSleepingDefaultDisplay;
/**
- * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is
+ * This is used to check whether to acquire screen-off sleep token when screen is
* turned off. E.g. if it is false when screen is turned off and the display is swapping, it
* is expected that the screen will be on in a short time. Then it is unnecessary to acquire
* screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes.
@@ -610,7 +610,6 @@
private boolean mPendingKeyguardOccluded;
private boolean mKeyguardOccludedChanged;
- private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer;
Intent mHomeIntent;
Intent mCarDockIntent;
Intent mDeskDockIntent;
@@ -2220,9 +2219,6 @@
mLockPatternUtils = new LockPatternUtils(mContext);
mLogger = new MetricsLogger();
- mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal
- .createSleepTokenAcquirer("ScreenOff");
-
Resources res = mContext.getResources();
mWakeOnDpadKeyPress =
res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress);
@@ -5521,13 +5517,6 @@
mRequestedOrSleepingDefaultDisplay = true;
mIsGoingToSleepDefaultDisplay = true;
- // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in
- // order but the methods run on different threads) and updateScreenOffSleepToken was
- // skipped. Then acquire sleep token if screen was off.
- if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()) {
- updateScreenOffSleepToken(true /* acquire */);
- }
-
if (mKeyguardDelegate != null) {
mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason);
}
@@ -5688,11 +5677,9 @@
if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
if (displayId == DEFAULT_DISPLAY) {
- if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay) {
- updateScreenOffSleepToken(true /* acquire */);
- }
+ final boolean acquireSleepToken = !isSwappingDisplay || mIsGoingToSleepDefaultDisplay;
mRequestedOrSleepingDefaultDisplay = false;
- mDefaultDisplayPolicy.screenTurnedOff();
+ mDefaultDisplayPolicy.screenTurnedOff(acquireSleepToken);
synchronized (mLock) {
if (mKeyguardDelegate != null) {
mKeyguardDelegate.onScreenTurnedOff();
@@ -5748,7 +5735,6 @@
if (displayId == DEFAULT_DISPLAY) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
0 /* cookie */);
- updateScreenOffSleepToken(false /* acquire */);
mDefaultDisplayPolicy.screenTurningOn(screenOnListener);
mBootAnimationDismissable = false;
@@ -6255,15 +6241,6 @@
}
}
- // TODO (multidisplay): Support multiple displays in WindowManagerPolicy.
- private void updateScreenOffSleepToken(boolean acquire) {
- if (acquire) {
- mScreenOffSleepTokenAcquirer.acquire(DEFAULT_DISPLAY);
- } else {
- mScreenOffSleepTokenAcquirer.release(DEFAULT_DISPLAY);
- }
- }
-
/** {@inheritDoc} */
@Override
public void enableScreenAfterBoot() {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index f53dda6..4dcc6e1 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3169,7 +3169,8 @@
final WallpaperDestinationChangeHandler
liveSync = new WallpaperDestinationChangeHandler(
newWallpaper);
- boolean same = changingToSame(name, newWallpaper);
+ boolean same = changingToSame(name, newWallpaper.connection,
+ newWallpaper.wallpaperComponent);
/*
* If we have a shared system+lock wallpaper, and we reapply the same wallpaper
@@ -3257,14 +3258,15 @@
return name == null || name.equals(mDefaultWallpaperComponent);
}
- private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) {
- if (wallpaper.connection != null) {
- final ComponentName wallpaperName = wallpaper.wallpaperComponent;
- if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) {
+ private boolean changingToSame(ComponentName newComponentName,
+ WallpaperConnection currentConnection, ComponentName currentComponentName) {
+ if (currentConnection != null) {
+ if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) {
if (DEBUG) Slog.v(TAG, "changingToSame: still using default");
// Still using default wallpaper.
return true;
- } else if (wallpaperName != null && wallpaperName.equals(componentName)) {
+ } else if (currentComponentName != null && currentComponentName.equals(
+ newComponentName)) {
// Changing to same wallpaper.
if (DEBUG) Slog.v(TAG, "same wallpaper");
return true;
@@ -3279,7 +3281,8 @@
Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName);
}
// Has the component changed?
- if (!force && changingToSame(componentName, wallpaper)) {
+ if (!force && changingToSame(componentName, wallpaper.connection,
+ wallpaper.wallpaperComponent)) {
try {
if (DEBUG_LIVE) {
Slog.v(TAG, "Changing to the same component, ignoring");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 99747e0..0be6471 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8149,7 +8149,8 @@
*/
@Override
protected int getOverrideOrientation() {
- if (mWmService.mConstants.mIgnoreActivityOrientationRequest) {
+ if (mWmService.mConstants.mIgnoreActivityOrientationRequest
+ && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
}
return mAppCompatController.getOrientationPolicy()
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index b8ce02ed..3d6b64b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -130,35 +130,6 @@
}
/**
- * Sleep tokens cause the activity manager to put the top activity to sleep.
- * They are used by components such as dreams that may hide and block interaction
- * with underlying activities.
- * The Acquirer provides an interface that encapsulates the underlying work, so the user does
- * not need to handle the token by him/herself.
- */
- public interface SleepTokenAcquirer {
-
- /**
- * Acquires a sleep token.
- * @param displayId The display to apply to.
- */
- void acquire(int displayId);
-
- /**
- * Releases the sleep token.
- * @param displayId The display to apply to.
- */
- void release(int displayId);
- }
-
- /**
- * Creates a sleep token acquirer for the specified display with the specified tag.
- *
- * @param tag A string identifying the purpose (eg. "Dream").
- */
- public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag);
-
- /**
* Returns home activity for the specified user.
*
* @param userId ID of the user or {@link android.os.UserHandle#USER_ALL}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index e25d940d..49ca698 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4356,6 +4356,7 @@
mTaskOrganizerController.dump(pw, " ");
mVisibleActivityProcessTracker.dump(pw, " ");
mActiveUids.dump(pw, " ");
+ pw.println(" SleepTokens=" + mRootWindowContainer.mSleepTokens);
if (mDemoteTopAppReasons != 0) {
pw.println(" mDemoteTopAppReasons=" + mDemoteTopAppReasons);
}
@@ -5071,17 +5072,16 @@
EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason);
}
- final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer {
+ final class SleepTokenAcquirer {
private final String mTag;
private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens =
new SparseArray<>();
- SleepTokenAcquirerImpl(@NonNull String tag) {
+ SleepTokenAcquirer(@NonNull String tag) {
mTag = tag;
}
- @Override
- public void acquire(int displayId) {
+ void acquire(int displayId) {
synchronized (mGlobalLock) {
if (!mSleepTokens.contains(displayId)) {
mSleepTokens.append(displayId,
@@ -5091,8 +5091,7 @@
}
}
- @Override
- public void release(int displayId) {
+ void release(int displayId) {
synchronized (mGlobalLock) {
final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId);
if (token != null) {
@@ -5955,11 +5954,6 @@
}
final class LocalService extends ActivityTaskManagerInternal {
- @Override
- public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) {
- Objects.requireNonNull(tag);
- return new SleepTokenAcquirerImpl(tag);
- }
@Override
public ComponentName getHomeActivityForUser(int userId) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index e8a3951..10e0641 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -735,8 +735,6 @@
/** All tokens used to put activities on this root task to sleep (including mOffToken) */
final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>();
- /** The token acquirer to put root tasks on the display to sleep */
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer;
private boolean mSleeping;
@@ -1131,7 +1129,6 @@
mDisplay = display;
mDisplayId = display.getDisplayId();
mCurrentUniqueDisplayId = display.getUniqueId();
- mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer;
mWallpaperController = new WallpaperController(mWmService, this);
mWallpaperController.resetLargestDisplay(display);
display.getDisplayInfo(mDisplayInfo);
@@ -6157,9 +6154,9 @@
final int displayState = mDisplayInfo.state;
if (displayId != DEFAULT_DISPLAY) {
if (displayState == Display.STATE_OFF) {
- mOffTokenAcquirer.acquire(mDisplayId);
+ mRootWindowContainer.mDisplayOffTokenAcquirer.acquire(mDisplayId);
} else if (displayState == Display.STATE_ON) {
- mOffTokenAcquirer.release(mDisplayId);
+ mRootWindowContainer.mDisplayOffTokenAcquirer.release(mDisplayId);
}
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
"Content Recording: Display %d state was (%d), is now (%d), so update "
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5c62120..107d31e 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -804,6 +804,14 @@
mAwake /* waiting */);
if (!awake) {
onDisplaySwitchFinished();
+ // In case PhoneWindowManager's startedGoingToSleep is called after screenTurnedOff
+ // (the source caller is in order but the methods run on different threads) and
+ // updateScreenOffSleepToken was skipped by mIsGoingToSleepDefaultDisplay. Then
+ // acquire sleep token if screen is off.
+ if (!mScreenOnEarly && !mScreenOnFully && !mDisplayContent.isSleeping()) {
+ Slog.w(TAG, "Late acquire sleep token for " + mDisplayContent);
+ mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+ }
}
}
}
@@ -851,6 +859,7 @@
public void screenTurningOn(ScreenOnListener screenOnListener) {
WindowProcessController visibleDozeUiProcess = null;
synchronized (mLock) {
+ mService.mRoot.mDisplayOffTokenAcquirer.release(mDisplayContent.mDisplayId);
mScreenOnEarly = true;
mScreenOnFully = false;
mKeyguardDrawComplete = false;
@@ -875,8 +884,12 @@
onDisplaySwitchFinished();
}
- public void screenTurnedOff() {
+ /** It is called after {@link #screenTurningOn}. This runs on PowerManager's thread. */
+ public void screenTurnedOff(boolean acquireSleepToken) {
synchronized (mLock) {
+ if (acquireSleepToken) {
+ mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+ }
mScreenOnEarly = false;
mScreenOnFully = false;
mKeyguardDrawComplete = false;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 5d8a96c..0c489d6 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -87,7 +87,7 @@
private final SparseArray<KeyguardDisplayState> mDisplayStates = new SparseArray<>();
private final ActivityTaskManagerService mService;
private RootWindowContainer mRootWindowContainer;
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+ private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
private boolean mWaitingForWakeTransition;
private Transition.ReadyCondition mWaitAodHide = null;
@@ -95,7 +95,7 @@
ActivityTaskSupervisor taskSupervisor) {
mService = service;
mTaskSupervisor = taskSupervisor;
- mSleepTokenAcquirer = mService.new SleepTokenAcquirerImpl(KEYGUARD_SLEEP_TOKEN_TAG);
+ mSleepTokenAcquirer = mService.new SleepTokenAcquirer(KEYGUARD_SLEEP_TOKEN_TAG);
}
void setWindowManager(WindowManagerService windowManager) {
@@ -658,10 +658,10 @@
private boolean mRequestDismissKeyguard;
private final ActivityTaskManagerService mService;
- private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+ private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
KeyguardDisplayState(ActivityTaskManagerService service, int displayId,
- ActivityTaskManagerInternal.SleepTokenAcquirer acquirer) {
+ ActivityTaskManagerService.SleepTokenAcquirer acquirer) {
mService = service;
mDisplayId = displayId;
mSleepTokenAcquirer = acquirer;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 866dcd5..8f5612c 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -215,7 +215,7 @@
private static final String DISPLAY_OFF_SLEEP_TOKEN_TAG = "Display-off";
/** The token acquirer to put root tasks on the displays to sleep */
- final ActivityTaskManagerInternal.SleepTokenAcquirer mDisplayOffTokenAcquirer;
+ final ActivityTaskManagerService.SleepTokenAcquirer mDisplayOffTokenAcquirer;
/**
* The modes which affect which tasks are returned when calling
@@ -450,7 +450,7 @@
mService = service.mAtmService;
mTaskSupervisor = mService.mTaskSupervisor;
mTaskSupervisor.mRootWindowContainer = this;
- mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+ mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirer(DISPLAY_OFF_SLEEP_TOKEN_TAG);
mDeviceStateController = new DeviceStateController(service.mContext, service.mGlobalLock);
mDisplayRotationCoordinator = new DisplayRotationCoordinator();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 5aa34d2..92953e5 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -17,6 +17,8 @@
package com.android.server.wm;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
@@ -206,7 +208,13 @@
mOrganizerPid = pid;
mAppThread = getAppThread(pid, mOrganizerUid);
for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
- mOrganizedTaskFragments.get(i).onTaskFragmentOrganizerRestarted(organizer);
+ final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
+ if (taskFragment.isAttached()
+ && taskFragment.getTopNonFinishingActivity() != null) {
+ taskFragment.onTaskFragmentOrganizerRestarted(organizer);
+ } else {
+ mOrganizedTaskFragments.remove(taskFragment);
+ }
}
try {
mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/);
@@ -575,8 +583,29 @@
}
mCachedTaskFragmentOrganizerStates.remove(cachedState);
- outSavedState.putAll(cachedState.mSavedState);
cachedState.restore(organizer, pid);
+ outSavedState.putAll(cachedState.mSavedState);
+
+ // Collect the organized TfInfo and TfParentInfo in the system.
+ final ArrayList<TaskFragmentInfo> infos = new ArrayList<>();
+ final ArrayMap<Integer, Task> tasks = new ArrayMap<>();
+ final int fragmentCount = cachedState.mOrganizedTaskFragments.size();
+ for (int j = 0; j < fragmentCount; j++) {
+ final TaskFragment tf = cachedState.mOrganizedTaskFragments.get(j);
+ infos.add(tf.getTaskFragmentInfo());
+ if (!tasks.containsKey(tf.getTask().mTaskId)) {
+ tasks.put(tf.getTask().mTaskId, tf.getTask());
+ }
+ }
+ outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENTS_INFO, infos);
+
+ final ArrayList<TaskFragmentParentInfo> parentInfos = new ArrayList<>();
+ for (int j = tasks.size() - 1; j >= 0; j--) {
+ parentInfos.add(tasks.valueAt(j).getTaskFragmentParentInfo());
+ }
+ outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+ parentInfos);
+
mTaskFragmentOrganizerState.put(organizer.asBinder(), cachedState);
mPendingTaskFragmentEvents.put(organizer.asBinder(), new ArrayList<>());
return true;
diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java
index 47c42f4..e0f24d8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerConstants.java
+++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java
@@ -34,7 +34,7 @@
*/
final class WindowManagerConstants {
- /** The orientation of activity will be always "unspecified". */
+ /** The orientation of activity will be always "unspecified" except for game apps. */
private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST =
"ignore_activity_orientation_request";
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
index 5233f19..a0f1a55 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
@@ -16,6 +16,7 @@
package com.android.server.appfunctions
import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE
import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
import android.app.appsearch.AppSearchManager
@@ -42,7 +43,7 @@
fun clearData() {
val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
- val setSchemaRequest = SetSchemaRequest.Builder().build()
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
it.setSchema(setSchemaRequest)
}
}
@@ -123,6 +124,38 @@
}
}
+ @Test
+ fun getByDocumentId() {
+ val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(
+ createParentAppFunctionRuntimeSchema(),
+ createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+ )
+ .build()
+ val schema = session.setSchema(setSchemaRequest)
+ val appFunctionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(appFunctionRuntimeMetadata)
+ .build()
+ val putResult = session.put(putDocumentsRequest)
+
+ val genricDocument = session
+ .getByDocumentId(
+ /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
+ APP_FUNCTION_RUNTIME_NAMESPACE
+ )
+ .get()
+
+ val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument)
+ assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID)
+ }
+ }
+
private companion object {
const val TEST_DB: String = "test_db"
const val TEST_PACKAGE_NAME: String = "test_pkg"
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
new file mode 100644
index 0000000..1fa55c7
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.server.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SetSchemaRequest
+import android.app.appsearch.observer.DocumentChangeInfo
+import android.app.appsearch.observer.ObserverCallback
+import android.app.appsearch.observer.ObserverSpec
+import android.app.appsearch.observer.SchemaChangeInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.infra.AndroidFuture
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FutureGlobalSearchSessionTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+ private val testExecutor = MoreExecutors.directExecutor()
+
+ @Before
+ @After
+ fun clearData() {
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+ it.setSchema(setSchemaRequest)
+ }
+ }
+
+ @Test
+ fun registerDocumentChangeObserverCallback() {
+ val packageObserverSpec: ObserverSpec =
+ ObserverSpec.Builder()
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val settableDocumentChangeInfo: AndroidFuture<DocumentChangeInfo> = AndroidFuture()
+ val observer: ObserverCallback =
+ object : ObserverCallback {
+ override fun onSchemaChanged(changeInfo: SchemaChangeInfo) {}
+
+ override fun onDocumentChanged(changeInfo: DocumentChangeInfo) {
+ settableDocumentChangeInfo.complete(changeInfo)
+ }
+ }
+ val futureGlobalSearchSession = FutureGlobalSearchSession(appSearchManager, testExecutor)
+
+ val registerPackageObserver: Void? =
+ futureGlobalSearchSession
+ .registerObserverCallbackAsync(
+ TEST_TARGET_PKG_NAME,
+ packageObserverSpec,
+ testExecutor,
+ observer,
+ )
+ .get()
+ assertThat(registerPackageObserver).isNull()
+ // Trigger document change
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(
+ createParentAppFunctionRuntimeSchema(),
+ createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME),
+ )
+ .build()
+ val schema = session.setSchema(setSchemaRequest)
+ assertThat(schema.get()).isNotNull()
+ val appFunctionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, TEST_FUNCTION_ID, "")
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(appFunctionRuntimeMetadata)
+ .build()
+ val putResult = session.put(putDocumentsRequest).get()
+ assertThat(putResult.isSuccess).isTrue()
+ }
+ assertThat(
+ settableDocumentChangeInfo
+ .get()
+ .changedDocumentIds
+ .contains(
+ AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(
+ TEST_TARGET_PKG_NAME,
+ TEST_FUNCTION_ID,
+ )
+ )
+ )
+ .isTrue()
+ }
+
+ private companion object {
+ const val TEST_DB: String = "test_db"
+ const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+ const val TEST_FUNCTION_ID: String = "print"
+ }
+}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
new file mode 100644
index 0000000..1061da2
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.server.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SearchSpec
+import android.app.appsearch.SetSchemaRequest
+import android.util.ArrayMap
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MetadataSyncAdapterTest {
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+ private val testExecutor = MoreExecutors.directExecutor()
+
+ @Before
+ @After
+ fun clearData() {
+ val searchContext = SearchContext.Builder(TEST_DB).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+ it.setSchema(setSchemaRequest)
+ }
+ }
+
+ @Test
+ fun getPackageToFunctionIdMap() {
+ val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+ val functionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+ .addSchemas(
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+ assertThat(setSchemaResponse).isNotNull()
+ val appSearchBatchResult = it.put(putDocumentsRequest).get()
+ assertThat(appSearchBatchResult.isSuccess).isTrue()
+ }
+
+ val metadataSyncAdapter =
+ MetadataSyncAdapter(
+ testExecutor,
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+ )
+ val searchSpec: SearchSpec =
+ SearchSpec.Builder()
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ .schemaType,
+ )
+ .build()
+ val packageToFunctionIdMap =
+ metadataSyncAdapter.getPackageToFunctionIdMap(
+ "",
+ searchSpec,
+ PROPERTY_FUNCTION_ID,
+ PROPERTY_PACKAGE_NAME,
+ )
+
+ assertThat(packageToFunctionIdMap).isNotNull()
+ assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId")
+ }
+
+ @Test
+ fun getPackageToFunctionIdMap_multipleDocuments() {
+ val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+ val functionRuntimeMetadata =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+ val functionRuntimeMetadata1 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build()
+ val functionRuntimeMetadata2 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build()
+ val functionRuntimeMetadata3 =
+ AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build()
+ val setSchemaRequest =
+ SetSchemaRequest.Builder()
+ .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+ .addSchemas(
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ )
+ .build()
+ val putDocumentsRequest: PutDocumentsRequest =
+ PutDocumentsRequest.Builder()
+ .addGenericDocuments(
+ functionRuntimeMetadata,
+ functionRuntimeMetadata1,
+ functionRuntimeMetadata2,
+ functionRuntimeMetadata3,
+ )
+ .build()
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+ val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+ assertThat(setSchemaResponse).isNotNull()
+ val appSearchBatchResult = it.put(putDocumentsRequest).get()
+ assertThat(appSearchBatchResult.isSuccess).isTrue()
+ }
+
+ val metadataSyncAdapter =
+ MetadataSyncAdapter(
+ testExecutor,
+ FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+ )
+ val searchSpec: SearchSpec =
+ SearchSpec.Builder()
+ .setResultCountPerPage(1)
+ .addFilterSchemas(
+ AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+ AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+ .schemaType,
+ )
+ .build()
+ val packageToFunctionIdMap =
+ metadataSyncAdapter.getPackageToFunctionIdMap(
+ "",
+ searchSpec,
+ PROPERTY_FUNCTION_ID,
+ PROPERTY_PACKAGE_NAME,
+ )
+
+ assertThat(packageToFunctionIdMap).isNotNull()
+ assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME])
+ .containsExactly(
+ "testFunctionId",
+ "testFunctionId1",
+ "testFunctionId2",
+ "testFunctionId3",
+ )
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_noDiff() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+ ArrayMap(staticPackageToFunctionMap)
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_addedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2")
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_addedFunctionNewPackage() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+ }
+
+ @Test
+ fun getAddedFunctionsDiffMap_removedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val addedFunctionsDiffMap =
+ MetadataSyncAdapter.getAddedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_noDiff() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+ ArrayMap(staticPackageToFunctionMap)
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_removedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ runtimePackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.size).isEqualTo(1)
+ assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+ }
+
+ @Test
+ fun getRemovedFunctionsDiffMap_addedFunction() {
+ val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+ staticPackageToFunctionMap.putAll(
+ mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+ )
+ val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+ val removedFunctionsDiffMap =
+ MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+ staticPackageToFunctionMap,
+ runtimePackageToFunctionMap,
+ )
+
+ assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+ }
+
+ private companion object {
+ const val TEST_DB: String = "test_db"
+ const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
new file mode 100644
index 0000000..c8e4f89
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job;
+
+import static android.app.job.Flags.FLAG_CLEANUP_EMPTY_JOBS;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+
+import android.app.job.IJobCallback;
+import android.app.job.JobParameters;
+import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class JobParametersTest {
+ private static final String TAG = JobParametersTest.class.getSimpleName();
+ private static final int TEST_JOB_ID_1 = 123;
+ private static final String TEST_NAMESPACE = "TEST_NAMESPACE";
+ private static final String TEST_DEBUG_STOP_REASON = "TEST_DEBUG_STOP_REASON";
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private MockitoSession mMockingSession;
+ @Mock private Parcel mMockParcel;
+ @Mock private IJobCallback.Stub mMockJobCallbackStub;
+
+ @Before
+ public void setUp() throws Exception {
+ mMockingSession =
+ mockitoSession().initMocks(this).strictness(Strictness.LENIENT).startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ if (mMockingSession != null) {
+ mMockingSession.finishMocking();
+ }
+
+ when(mMockParcel.readInt())
+ .thenReturn(TEST_JOB_ID_1) // Job ID
+ .thenReturn(0) // No clip data
+ .thenReturn(0) // No deadline expired
+ .thenReturn(0) // No network
+ .thenReturn(0) // No stop reason
+ .thenReturn(0); // Internal stop reason
+ when(mMockParcel.readString())
+ .thenReturn(TEST_NAMESPACE) // Job namespace
+ .thenReturn(TEST_DEBUG_STOP_REASON); // Debug stop reason
+ when(mMockParcel.readPersistableBundle()).thenReturn(null);
+ when(mMockParcel.readBundle()).thenReturn(null);
+ when(mMockParcel.readStrongBinder()).thenReturn(mMockJobCallbackStub);
+ when(mMockParcel.readBoolean())
+ .thenReturn(false) // expedited
+ .thenReturn(false); // user initiated
+ when(mMockParcel.createTypedArray(any())).thenReturn(new Uri[0]);
+ when(mMockParcel.createStringArray()).thenReturn(new String[0]);
+ }
+
+ /**
+ * Test to verify that the JobParameters created using Non-Parcelable constructor has not
+ * cleaner attached
+ */
+ @Test
+ public void testJobParametersNonParcelableConstructor_noCleaner() {
+ JobParameters jobParameters =
+ new JobParameters(
+ null,
+ TEST_NAMESPACE,
+ TEST_JOB_ID_1,
+ null,
+ null,
+ null,
+ 0,
+ false,
+ false,
+ false,
+ null,
+ null,
+ null);
+
+ // Verify that cleaner is not registered
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+
+ /**
+ * Test to verify that the JobParameters created using Parcelable constructor has not cleaner
+ * attached
+ */
+ @Test
+ public void testJobParametersParcelableConstructor_noCleaner() {
+ JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+ // Verify that cleaner is not registered
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+
+ /** Test to verify that the JobParameters Cleaner is disabled */
+ @RequiresFlagsEnabled(FLAG_CLEANUP_EMPTY_JOBS)
+ @Test
+ public void testCleanerWithLeakedJobCleanerDisabled_flagCleanupEmptyJobsEnabled() {
+ // Inject real JobCallbackCleanup
+ JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+ // Enable the cleaner
+ jobParameters.enableCleaner();
+
+ // Verify the cleaner is enabled
+ assertThat(jobParameters.getCleanable()).isNotNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNotNull();
+ assertThat(jobParameters.getJobCleanupCallback().isCleanerEnabled()).isTrue();
+
+ // Disable the cleaner
+ jobParameters.disableCleaner();
+
+ // Verify the cleaner is disabled
+ assertThat(jobParameters.getCleanable()).isNull();
+ assertThat(jobParameters.getJobCleanupCallback()).isNull();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index a5f1fcd..2d95740 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -30,6 +30,7 @@
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS;
import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS;
+import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
import static com.google.common.truth.Truth.assertThat;
@@ -273,13 +274,12 @@
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
// Finish querying SADs
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
mNativeWrapper.clearResultMessages();
@@ -685,18 +685,46 @@
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
// Finish querying SADs
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
}
@Test
+ public void handleInitiateArc_arcAlreadyEstablished_noRequestSad() {
+ // Emulate Audio device on port 0x2000 (supports ARC)
+ mNativeWrapper.setPortConnectionStatus(2, true);
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isFalse();
+
+ HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+ ADDR_AUDIO_SYSTEM,
+ ADDR_TV);
+ mNativeWrapper.onCecMessage(requestArcInitiation);
+ mTestLooper.dispatchAll();
+
+ // Finish querying SADs
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
+
+ assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isTrue();
+ }
+
+ @Test
public void handleTerminateArc_noAudioDevice() {
HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
ADDR_AUDIO_SYSTEM,
@@ -970,13 +998,12 @@
// <Report ARC Initiated> should only be sent after SAD querying is done
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
// Finish querying SADs
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
mNativeWrapper.clearResultMessages();
@@ -1171,13 +1198,12 @@
mTestLooper.dispatchAll();
// Finish querying SADs
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
// ARC should be established after RequestSadAction is finished
assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
@@ -1327,13 +1353,12 @@
assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
// Finish querying SADs
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
- mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
- mTestLooper.dispatchAll();
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index f8e465c..4cf2937 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -18,6 +18,7 @@
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
+import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
import static com.google.common.truth.Truth.assertThat;
@@ -144,7 +145,7 @@
}
@Test
- public void noResponse_queryAgainOnce_emptyResult() {
+ public void noResponse_queryAgain_emptyResult() {
RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
mCallback);
action.start();
@@ -154,13 +155,13 @@
HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
CODECS_TO_QUERY_1.stream().mapToInt(i -> i).toArray());
- assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
- mTestLooper.moveTimeForward(TIMEOUT_MS);
- mTestLooper.dispatchAll();
+
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mSupportedSads).isNotNull();
assertThat(mSupportedSads.size()).isEqualTo(0);
@@ -507,7 +508,7 @@
}
@Test
- public void invalidMessageLength_queryAgainOnce() {
+ public void invalidMessageLength_queryAgain() {
RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
mCallback);
action.start();
@@ -524,16 +525,13 @@
0x27, 0x20, 0x0A};
HdmiCecMessage response1 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_1);
- assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
- mNativeWrapper.clearResultMessages();
- action.processCommand(response1);
- mTestLooper.dispatchAll();
- mTestLooper.moveTimeForward(TIMEOUT_MS);
- mTestLooper.dispatchAll();
- assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
- mNativeWrapper.clearResultMessages();
- mTestLooper.moveTimeForward(TIMEOUT_MS);
- mTestLooper.dispatchAll();
+
+ for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+ assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
+ mNativeWrapper.clearResultMessages();
+ mTestLooper.moveTimeForward(TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+ }
assertThat(mSupportedSads).isNotNull();
assertThat(mSupportedSads.size()).isEqualTo(0);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 4a19973..1890879 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -39,6 +39,7 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.KeyguardManager;
import android.app.UiModeManager;
import android.app.WallpaperManager;
import android.content.BroadcastReceiver;
@@ -78,6 +79,7 @@
private DefaultDeviceEffectsApplier mApplier;
@Mock PowerManager mPowerManager;
@Mock ColorDisplayManager mColorDisplayManager;
+ @Mock KeyguardManager mKeyguardManager;
@Mock UiModeManager mUiModeManager;
@Mock WallpaperManager mWallpaperManager;
@@ -87,6 +89,7 @@
mContext = spy(new TestableContext(InstrumentationRegistry.getContext(), null));
mContext.addMockSystemService(PowerManager.class, mPowerManager);
mContext.addMockSystemService(ColorDisplayManager.class, mColorDisplayManager);
+ mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
mContext.addMockSystemService(UiModeManager.class, mUiModeManager);
mContext.addMockSystemService(WallpaperManager.class, mWallpaperManager);
when(mWallpaperManager.isWallpaperSupported()).thenReturn(true);
@@ -311,6 +314,22 @@
}
@Test
+ @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+ public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately(
+ @TestParameter ZenChangeOrigin origin) {
+
+ when(mPowerManager.isInteractive()).thenReturn(true);
+ when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+ mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
+ origin.value());
+
+ // Effect was applied, and no broadcast receiver was registered.
+ verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT));
+ verify(mContext, never()).registerReceiver(any(), any(), anyInt());
+ }
+
+ @Test
@TestParameters({"{origin: ORIGIN_USER_IN_SYSTEMUI}", "{origin: ORIGIN_USER_IN_APP}",
"{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"})
public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index e694c0b..536dcfb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -42,7 +42,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import android.app.ActivityManager;
import android.app.AppOpsManager;
@@ -135,15 +134,13 @@
doNothing().when(mPhoneWindowManager).initializeHdmiState();
final boolean[] isScreenTurnedOff = { false };
final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
- doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff();
+ doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(
+ anyBoolean());
doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
- final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer =
- mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class);
- doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString());
final PowerManager pm = mock(PowerManager.class);
doReturn(true).when(pm).isInteractive();
doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
@@ -155,9 +152,8 @@
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
// Skip sleep-token for non-sleep-screen-off.
- clearInvocations(tokenAcquirer);
mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer, never()).acquire(anyInt());
+ verify(displayPolicy).screenTurnedOff(false /* acquireSleepToken */);
assertThat(isScreenTurnedOff[0]).isTrue();
// Apply sleep-token for sleep-screen-off.
@@ -165,21 +161,10 @@
mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
+ verify(displayPolicy).screenTurnedOff(true /* acquireSleepToken */);
mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
-
- // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep
- // token can still be acquired.
- isScreenTurnedOff[0] = false;
- clearInvocations(tokenAcquirer);
- mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
- verify(tokenAcquirer, never()).acquire(anyInt());
- assertThat(displayPolicy.isScreenOnEarly()).isFalse();
- assertThat(displayPolicy.isScreenOnFully()).isFalse();
- mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
- verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 1e035da..e2e76d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3231,7 +3231,7 @@
mDisplayContent.mOpeningApps.remove(activity);
mDisplayContent.mClosingApps.remove(activity);
activity.commitVisibility(false /* visible */, false /* performLayout */);
- mDisplayContent.getDisplayPolicy().screenTurnedOff();
+ mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */);
final KeyguardController controller = mSupervisor.getKeyguardController();
doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
activity.setVisibility(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index caeb41c..f32a234 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -284,11 +284,11 @@
final DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
policy.addWindowLw(mNotificationShadeWindow, mNotificationShadeWindow.mAttrs);
- policy.screenTurnedOff();
+ policy.screenTurnedOff(false /* acquireSleepToken */);
policy.setAwake(false);
policy.screenTurningOn(null /* screenOnListener */);
assertTrue(wpc.isShowingUiWhileDozing());
- policy.screenTurnedOff();
+ policy.screenTurnedOff(false /* acquireSleepToken */);
assertFalse(wpc.isShowingUiWhileDozing());
policy.screenTurningOn(null /* screenOnListener */);
@@ -393,7 +393,7 @@
info.logicalWidth, info.logicalHeight).mConfigFrame);
// If screen is not fully turned on, then the cache should be preserved.
- displayPolicy.screenTurnedOff();
+ displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
final TransitionController transitionController = mDisplayContent.mTransitionController;
spyOn(transitionController);
doReturn(true).when(transitionController).isCollecting();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index cc1805a..fd959b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -242,7 +242,7 @@
final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
- displayPolicy.screenTurnedOff();
+ displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
assertFalse(mTaskFragment.okToAnimate());
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 3944b8e..284e2bd 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -420,6 +420,14 @@
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28;
+ /**
+ * Enabling satellite is in progress.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29;
+
/** @hide */
@IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
SATELLITE_RESULT_SUCCESS,
@@ -450,7 +458,8 @@
SATELLITE_RESULT_LOCATION_DISABLED,
SATELLITE_RESULT_LOCATION_NOT_AVAILABLE,
SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS,
- SATELLITE_RESULT_DISABLE_IN_PROGRESS
+ SATELLITE_RESULT_DISABLE_IN_PROGRESS,
+ SATELLITE_RESULT_ENABLE_IN_PROGRESS
})
@Retention(RetentionPolicy.SOURCE)
public @interface SatelliteResult {}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index e852e6b..e57c207 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3394,4 +3394,19 @@
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
+
+ /**
+ * This API can be used by only CTS to override the cached value for the device overlay config
+ * value :
+ * config_satellite_gateway_service_package and
+ * config_satellite_carrier_roaming_esos_provisioned_class.
+ * These values are set before sending an intent to broadcast there are any change to list of
+ * subscriber informations.
+ *
+ * @param name the name is one of the following that constitute an intent.
+ * Component package name, or component class name.
+ * @return {@code true} if the setting is successful, {@code false} otherwise.
+ * @hide
+ */
+ boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name);
}
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index ad0ef1b..0f08be2 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,9 @@
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
import android.testing.TestableContext;
import android.view.MotionEvent;
import android.view.View;
@@ -40,6 +42,8 @@
import com.android.cts.input.MotionEventBuilder;
import com.android.cts.input.PointerBuilder;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
import org.junit.Before;
import org.junit.Test;
@@ -289,4 +293,36 @@
assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x);
assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y);
}
+
+ @Test
+ public void testTouchpadClick() {
+ View child;
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+ }
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
+ }
+
+ mTouchpadDebugView.updateHardwareState(
+ new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+ new TouchpadFingerState[0]));
+
+ for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+ child = mTouchpadDebugView.getChildAt(i);
+ assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+ }
+ }
}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
similarity index 76%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
index 4b9cf85..bc9471b 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.ravenwood.annotation;
+package android.hosttest.annotation;
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -24,13 +24,9 @@
/**
* THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
* QUESTIONS ABOUT IT.
- *
- * TODO: Javadoc
- *
* @hide
*/
-@Target({TYPE})
+@Target({METHOD})
@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
- String value();
+public @interface HostSideTestRedirect {
}
diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
similarity index 95%
rename from tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java
rename to tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
index 9c81383..28ad236 100644
--- a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
@@ -30,6 +30,6 @@
*/
@Target({TYPE})
@Retention(RetentionPolicy.CLASS)
-public @interface HostSideTestNativeSubstitutionClass {
+public @interface HostSideTestRedirectionClass {
String value();
}
diff --git a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
index e72c9a4..eba8e62 100644
--- a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
+++ b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
@@ -27,8 +27,11 @@
--substitute-annotation
android.hosttest.annotation.HostSideTestSubstitute
---native-substitute-annotation
- android.hosttest.annotation.HostSideTestNativeSubstitutionClass
+--redirect-annotation
+ android.hosttest.annotation.HostSideTestRedirect
+
+--redirection-class-annotation
+ android.hosttest.annotation.HostSideTestRedirectionClass
--class-load-hook-annotation
android.hosttest.annotation.HostSideTestClassLoadHook
diff --git a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
index 5f0368a..084448d 100755
--- a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
+++ b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
@@ -100,8 +100,10 @@
android.hosttest.annotation.HostSideTestRemove \
--substitute-annotation \
android.hosttest.annotation.HostSideTestSubstitute \
- --native-substitute-annotation \
- android.hosttest.annotation.HostSideTestNativeSubstitutionClass \
+ --redirect-annotation \
+ android.hosttest.annotation.HostSideTestRedirect \
+ --redirection-class-annotation \
+ android.hosttest.annotation.HostSideTestRedirectionClass \
--class-load-hook-annotation \
android.hosttest.annotation.HostSideTestClassLoadHook \
--keep-static-initializer-annotation \
@@ -223,4 +225,4 @@
echo "All tests passed"
-exit 0
\ No newline at end of file
+exit 0
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 0f38fe7..34aaaa9 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -24,8 +24,9 @@
import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.filters.FilterRemapper
import com.android.hoststubgen.filters.ImplicitOutputFilter
-import com.android.hoststubgen.filters.NativeFilter
+import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
+import com.android.hoststubgen.filters.SanitizationFilter
import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
import com.android.hoststubgen.filters.printAsTextPolicy
import com.android.hoststubgen.utils.ClassFilter
@@ -134,7 +135,7 @@
var filter: OutputFilter = ConstantFilter(options.defaultPolicy.get, "default-by-options")
// Next, we build a filter that preserves all native methods by default
- filter = NativeFilter(allClasses, filter)
+ filter = KeepNativeFilter(allClasses, filter)
// Next, we need a filter that resolves "class-wide" policies.
// This is used when a member (methods, fields, nested classes) don't get any polices
@@ -166,11 +167,12 @@
options.throwAnnotations,
options.removeAnnotations,
options.substituteAnnotations,
- options.nativeSubstituteAnnotations,
+ options.redirectAnnotations,
+ options.redirectionClassAnnotations,
options.classLoadHookAnnotations,
options.keepStaticInitializerAnnotations,
annotationAllowedClassesFilter,
- filter,
+ filter
)
// Next, "text based" filter, which allows to override polices without touching
@@ -182,6 +184,9 @@
// Apply the implicit filter.
filter = ImplicitOutputFilter(errors, allClasses, filter)
+ // Add a final sanitization step.
+ filter = SanitizationFilter(errors, allClasses, filter)
+
return filter
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 1cedcc3..057a52c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -85,9 +85,10 @@
var throwAnnotations: MutableSet<String> = mutableSetOf(),
var removeAnnotations: MutableSet<String> = mutableSetOf(),
var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
+ var redirectAnnotations: MutableSet<String> = mutableSetOf(),
var substituteAnnotations: MutableSet<String> = mutableSetOf(),
- var nativeSubstituteAnnotations: MutableSet<String> = mutableSetOf(),
+ var redirectionClassAnnotations: MutableSet<String> = mutableSetOf(),
var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(),
var keepStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(),
@@ -186,8 +187,11 @@
"--substitute-annotation" ->
ret.substituteAnnotations.addUniqueAnnotationArg()
- "--native-substitute-annotation" ->
- ret.nativeSubstituteAnnotations.addUniqueAnnotationArg()
+ "--redirect-annotation" ->
+ ret.redirectAnnotations.addUniqueAnnotationArg()
+
+ "--redirection-class-annotation" ->
+ ret.redirectionClassAnnotations.addUniqueAnnotationArg()
"--class-load-hook-annotation" ->
ret.classLoadHookAnnotations.addUniqueAnnotationArg()
@@ -275,7 +279,7 @@
removeAnnotations=$removeAnnotations,
keepClassAnnotations=$keepClassAnnotations,
substituteAnnotations=$substituteAnnotations,
- nativeSubstituteAnnotations=$nativeSubstituteAnnotations,
+ nativeSubstituteAnnotations=$redirectionClassAnnotations,
classLoadHookAnnotations=$classLoadHookAnnotations,
keepStaticInitializerAnnotations=$keepStaticInitializerAnnotations,
packageRedirects=$packageRedirects,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
index 7197e0e..a02082d 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
@@ -29,33 +29,24 @@
/** Name of the class initializer method. */
-val CLASS_INITIALIZER_NAME = "<clinit>"
+const val CLASS_INITIALIZER_NAME = "<clinit>"
/** Descriptor of the class initializer method. */
-val CLASS_INITIALIZER_DESC = "()V"
+const val CLASS_INITIALIZER_DESC = "()V"
/** Name of constructors. */
-val CTOR_NAME = "<init>"
+const val CTOR_NAME = "<init>"
/**
- * Find any of [anyAnnotations] from the list of visible / invisible annotations.
+ * Find any of [set] from the list of visible / invisible annotations.
*/
fun findAnyAnnotation(
- anyAnnotations: Set<String>,
- visibleAnnotations: List<AnnotationNode>?,
- invisibleAnnotations: List<AnnotationNode>?,
- ): AnnotationNode? {
- for (an in visibleAnnotations ?: emptyList()) {
- if (anyAnnotations.contains(an.desc)) {
- return an
- }
- }
- for (an in invisibleAnnotations ?: emptyList()) {
- if (anyAnnotations.contains(an.desc)) {
- return an
- }
- }
- return null
+ set: Set<String>,
+ visibleAnnotations: List<AnnotationNode>?,
+ invisibleAnnotations: List<AnnotationNode>?,
+): AnnotationNode? {
+ return visibleAnnotations?.find { it.desc in set }
+ ?: invisibleAnnotations?.find { it.desc in set }
}
fun ClassNode.findAnyAnnotation(set: Set<String>): AnnotationNode? {
@@ -70,6 +61,27 @@
return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations)
}
+fun findAllAnnotations(
+ set: Set<String>,
+ visibleAnnotations: List<AnnotationNode>?,
+ invisibleAnnotations: List<AnnotationNode>?
+): List<AnnotationNode> {
+ return (visibleAnnotations ?: emptyList()).filter { it.desc in set } +
+ (invisibleAnnotations ?: emptyList()).filter { it.desc in set }
+}
+
+fun ClassNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+ return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun MethodNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+ return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun FieldNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+ return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
fun <T> findAnnotationValueAsObject(
an: AnnotationNode,
propertyName: String,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index 38a41b2..a6b8cdb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -18,12 +18,15 @@
import com.android.hoststubgen.ClassParseException
import com.android.hoststubgen.HostStubGenErrors
import com.android.hoststubgen.InvalidAnnotationException
-import com.android.hoststubgen.addNonNullElement
+import com.android.hoststubgen.addLists
import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAllAnnotations
import com.android.hoststubgen.asm.findAnnotationValueAsString
import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.getPackageNameFromFullClassName
+import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.asm.toHumanReadableMethodName
import com.android.hoststubgen.asm.toJvmClassName
@@ -46,7 +49,8 @@
throwAnnotations_: Set<String>,
removeAnnotations_: Set<String>,
substituteAnnotations_: Set<String>,
- nativeSubstituteAnnotations_: Set<String>,
+ redirectAnnotations_: Set<String>,
+ redirectionClassAnnotations_: Set<String>,
classLoadHookAnnotations_: Set<String>,
keepStaticInitializerAnnotations_: Set<String>,
private val annotationAllowedClassesFilter: ClassFilter,
@@ -56,8 +60,10 @@
private val keepClassAnnotations = convertToInternalNames(keepClassAnnotations_)
private val throwAnnotations = convertToInternalNames(throwAnnotations_)
private val removeAnnotations = convertToInternalNames(removeAnnotations_)
+ private val redirectAnnotations = convertToInternalNames(redirectAnnotations_)
private val substituteAnnotations = convertToInternalNames(substituteAnnotations_)
- private val nativeSubstituteAnnotations = convertToInternalNames(nativeSubstituteAnnotations_)
+ private val redirectionClassAnnotations =
+ convertToInternalNames(redirectionClassAnnotations_)
private val classLoadHookAnnotations = convertToInternalNames(classLoadHookAnnotations_)
private val keepStaticInitializerAnnotations =
convertToInternalNames(keepStaticInitializerAnnotations_)
@@ -67,11 +73,12 @@
keepClassAnnotations +
throwAnnotations +
removeAnnotations +
+ redirectAnnotations +
substituteAnnotations
/** All the annotations we use. */
private val allAnnotations = visibilityAnnotations +
- nativeSubstituteAnnotations +
+ redirectionClassAnnotations +
classLoadHookAnnotations +
keepStaticInitializerAnnotations
@@ -84,8 +91,9 @@
keepClassAnnotations_ +
throwAnnotations_ +
removeAnnotations_ +
+ redirectAnnotations_ +
substituteAnnotations_ +
- nativeSubstituteAnnotations_ +
+ redirectionClassAnnotations_ +
classLoadHookAnnotations_ +
keepStaticInitializerAnnotations_
)
@@ -99,6 +107,7 @@
in substituteAnnotations -> FilterPolicy.Substitute.withReason(REASON_ANNOTATION)
in throwAnnotations -> FilterPolicy.Throw.withReason(REASON_ANNOTATION)
in removeAnnotations -> FilterPolicy.Remove.withReason(REASON_ANNOTATION)
+ in redirectAnnotations -> FilterPolicy.Redirect.withReason(REASON_ANNOTATION)
else -> null
}
}
@@ -129,13 +138,6 @@
descriptor: String
): FilterPolicyWithReason {
val cn = classes.getClass(className)
-
- if (methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
- if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) {
- return FilterPolicy.Keep.withReason(REASON_ANNOTATION)
- }
- }
-
return getAnnotationPolicy(cn).methodPolicies[MethodKey(methodName, descriptor)]
?: super.getPolicyForMethod(className, methodName, descriptor)
}
@@ -150,22 +152,14 @@
?: super.getRenameTo(className, methodName, descriptor)
}
- override fun getNativeSubstitutionClass(className: String): String? {
- classes.getClass(className).let { cn ->
- cn.findAnyAnnotation(nativeSubstituteAnnotations)?.let { an ->
- return getAnnotationField(an, "value")?.toJvmClassName()
- }
- }
- return null
+ override fun getRedirectionClass(className: String): String? {
+ val cn = classes.getClass(className)
+ return getAnnotationPolicy(cn).redirectionClass
}
override fun getClassLoadHooks(className: String): List<String> {
- val e = classes.getClass(className).let { cn ->
- cn.findAnyAnnotation(classLoadHookAnnotations)?.let { an ->
- getAnnotationField(an, "value")?.toHumanReadableMethodName()
- }
- }
- return addNonNullElement(super.getClassLoadHooks(className), e)
+ val cn = classes.getClass(className)
+ return addLists(super.getClassLoadHooks(className), getAnnotationPolicy(cn).classLoadHooks)
}
private data class MethodKey(val name: String, val desc: String)
@@ -195,6 +189,8 @@
val fieldPolicies = mutableMapOf<String, FilterPolicyWithReason>()
val methodPolicies = mutableMapOf<MethodKey, FilterPolicyWithReason>()
val renamedMethods = mutableMapOf<MethodKey, String>()
+ val redirectionClass: String?
+ val classLoadHooks: List<String>
init {
val allowAnnotation = annotationAllowedClassesFilter.matches(cn.name)
@@ -204,6 +200,16 @@
"class", cn.name
)
classPolicy = cn.findAnyAnnotation(visibilityAnnotations)?.policy
+ redirectionClass = cn.findAnyAnnotation(redirectionClassAnnotations)?.let { an ->
+ getAnnotationField(an, "value")?.let { resolveRelativeClass(cn, it) }
+ }
+ classLoadHooks = cn.findAllAnnotations(classLoadHookAnnotations).mapNotNull { an ->
+ getAnnotationField(an, "value")?.toHumanReadableMethodName()
+ }
+ if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) {
+ methodPolicies[MethodKey(CLASS_INITIALIZER_NAME, CLASS_INITIALIZER_DESC)] =
+ FilterPolicy.Keep.withReason(REASON_ANNOTATION)
+ }
for (fn in cn.fields ?: emptyList()) {
detectInvalidAnnotations(
@@ -297,25 +303,36 @@
)
}
}
- }
- /**
- * Return the (String) value of 'value' parameter from an annotation.
- */
- private fun getAnnotationField(
- an: AnnotationNode,
- name: String,
- required: Boolean = true
- ): String? {
- try {
- val suffix = findAnnotationValueAsString(an, name)
- if (suffix == null && required) {
- errors.onErrorFound("Annotation \"${an.desc}\" must have field $name")
+ /**
+ * Return the (String) value of 'value' parameter from an annotation.
+ */
+ private fun getAnnotationField(
+ an: AnnotationNode,
+ name: String,
+ required: Boolean = true
+ ): String? {
+ try {
+ val suffix = findAnnotationValueAsString(an, name)
+ if (suffix == null && required) {
+ errors.onErrorFound("Annotation \"${an.desc}\" must have field $name")
+ }
+ return suffix
+ } catch (e: ClassParseException) {
+ errors.onErrorFound(e.message!!)
+ return null
}
- return suffix
- } catch (e: ClassParseException) {
- errors.onErrorFound(e.message!!)
- return null
+ }
+
+ /**
+ * Resolve the full class name if the class is relative
+ */
+ private fun resolveRelativeClass(
+ cn: ClassNode,
+ name: String
+ ): String {
+ val packageName = getPackageNameFromFullClassName(cn.name)
+ return resolveClassNameWithDefaultPackage(name, packageName).toJvmClassName()
}
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
index 8ee3a94..f8bb526 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
@@ -16,7 +16,6 @@
package com.android.hoststubgen.filters
import com.android.hoststubgen.asm.ClassNodes
-import com.android.hoststubgen.asm.isNative
/**
* This is used as the second last fallback filter. This filter propagates the class-wide policy
@@ -88,16 +87,7 @@
methodName: String,
descriptor: String
): FilterPolicyWithReason {
- return outermostFilter.getNativeSubstitutionClass(className)?.let {
- // First check native substitution
- classes.findMethod(className, methodName, descriptor)?.let { mn ->
- if (mn.isNative()) {
- FilterPolicy.NativeSubstitute.withReason("class-wide in $className")
- } else {
- null
- }
- }
- } ?: getClassWidePolicy(className, resolve = true)
- ?: super.getPolicyForMethod(className, methodName, descriptor)
+ return getClassWidePolicy(className, resolve = true)
+ ?: super.getPolicyForMethod(className, methodName, descriptor)
}
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
index 6fcffb8..b8b0d8a 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -72,8 +72,8 @@
return fallback.getRenameTo(className, methodName, descriptor)
}
- override fun getNativeSubstitutionClass(className: String): String? {
- return fallback.getNativeSubstitutionClass(className)
+ override fun getRedirectionClass(className: String): String? {
+ return fallback.getRedirectionClass(className)
}
override fun getClassLoadHooks(className: String): List<String> {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
index ab03874..2f2f81b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -33,9 +33,9 @@
Substitute,
/**
- * Only usable with methods. Replace a native method with a "substitution" method,
+ * Only usable with methods. Redirect a method to a method in the substitution class.
*/
- NativeSubstitute,
+ Redirect,
/**
* Only usable with methods. The item will be kept in the impl jar file, but when called,
@@ -102,8 +102,7 @@
val isSupported: Boolean
get() {
return when (this) {
- // TODO: handle native method with no substitution as being unsupported
- Keep, KeepClass, Substitute, NativeSubstitute -> true
+ Keep, KeepClass, Substitute, Redirect -> true
else -> false
}
}
@@ -111,7 +110,7 @@
val isMethodRewriteBody: Boolean
get() {
return when (this) {
- NativeSubstitute, Throw, Ignore -> true
+ Redirect, Throw, Ignore -> true
else -> false
}
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index 2e144f5..59fa464 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -19,6 +19,7 @@
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.asm.toHumanReadableMethodName
+import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.log
// TODO: Validate all input names.
@@ -29,7 +30,7 @@
) : DelegatingFilter(fallback) {
private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
private val mRenames: MutableMap<String, String> = mutableMapOf()
- private val mNativeSubstitutionClasses: MutableMap<String, String> = mutableMapOf()
+ private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
private fun getClassKey(className: String): String {
@@ -115,17 +116,17 @@
mRenames[getMethodKey(className, methodName, descriptor)] = toName
}
- override fun getNativeSubstitutionClass(className: String): String? {
- return mNativeSubstitutionClasses[getClassKey(className)]
- ?: super.getNativeSubstitutionClass(className)
+ override fun getRedirectionClass(className: String): String? {
+ return mRedirectionClasses[getClassKey(className)]
+ ?: super.getRedirectionClass(className)
}
- fun setNativeSubstitutionClass(from: String, to: String) {
+ fun setRedirectionClass(from: String, to: String) {
checkClass(from)
- // Native substitute classes may be provided from other jars, so we can't do this check.
+ // Redirection classes may be provided from other jars, so we can't do this check.
// ensureClassExists(to)
- mNativeSubstitutionClasses[getClassKey(from)] = to.toHumanReadableClassName()
+ mRedirectionClasses[getClassKey(from)] = to.toJvmClassName()
}
override fun getClassLoadHooks(className: String): List<String> {
@@ -136,4 +137,4 @@
fun setClassLoadHook(className: String, methodName: String) {
mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
}
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
similarity index 82%
rename from tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt
rename to tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
index bd71931..00e7d77 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
@@ -18,7 +18,12 @@
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.isNative
-class NativeFilter(
+/**
+ * For native methods that weren't handled by outer filters, we keep it so that
+ * native method registration will not crash at runtime. Ideally we shouldn't need
+ * this, but in practice unsupported native method registrations do occur.
+ */
+class KeepNativeFilter(
private val classes: ClassNodes,
fallback: OutputFilter
) : DelegatingFilter(fallback) {
@@ -28,8 +33,6 @@
descriptor: String,
): FilterPolicyWithReason {
return classes.findMethod(className, methodName, descriptor)?.let { mn ->
- // For native methods that weren't handled by outer filters,
- // we keep it so that native method registration will not crash.
if (mn.isNative()) {
FilterPolicy.Keep.withReason("native-preserve")
} else {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
index 1049e2b..f99ce90 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
@@ -35,10 +35,6 @@
* using it.
*/
open var outermostFilter: OutputFilter = this
- get() = field
- set(value) {
- field = value
- }
abstract fun getPolicyForClass(className: String): FilterPolicyWithReason
@@ -60,13 +56,13 @@
}
/**
- * Return a "native substitution class" name for a given class.
+ * Return a "redirection class" name for a given class.
*
- * The result will be in a "human readable" form. (e.g. uses '.'s instead of '/'s)
+ * The result will be in a JVM internal form. (e.g. uses '/'s instead of '.'s)
*
- * (which corresponds to @HostSideTestNativeSubstitutionClass of the standard annotations.)
+ * (which corresponds to @HostSideTestRedirectClass of the standard annotations.)
*/
- open fun getNativeSubstitutionClass(className: String): String? {
+ open fun getRedirectionClass(className: String): String? {
return null
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt
new file mode 100644
index 0000000..18a1e16
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.filters
+
+import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+
+/**
+ * Check whether the policies in the inner layers make sense, and sanitize the results.
+ */
+class SanitizationFilter(
+ private val errors: HostStubGenErrors,
+ private val classes: ClassNodes,
+ fallback: OutputFilter
+) : DelegatingFilter(fallback) {
+ override fun getPolicyForMethod(
+ className: String,
+ methodName: String,
+ descriptor: String
+ ): FilterPolicyWithReason {
+ val policy = super.getPolicyForMethod(className, methodName, descriptor)
+ if (policy.policy == FilterPolicy.Redirect) {
+ // Check whether the hosting class has a redirection class
+ if (getRedirectionClass(className) == null) {
+ errors.onErrorFound("Method $methodName$descriptor requires a redirection " +
+ "class set on ${className.toHumanReadableClassName()}")
+ }
+ }
+ return policy
+ }
+
+ override fun getRedirectionClass(className: String): String? {
+ return super.getRedirectionClass(className)?.also { clazz ->
+ if (classes.findClass(clazz) == null) {
+ log.w("Redirection class $clazz not found. Class must be available at runtime.")
+ } else if (outermostFilter.getPolicyForClass(clazz).policy != FilterPolicy.KeepClass) {
+ // If the class exists, it must have a KeepClass policy.
+ errors.onErrorFound("Redirection class $clazz must have @KeepWholeClass.")
+ }
+ }
+ }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 14fd82b..073b503 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -142,9 +142,9 @@
throw ParseException(
"Special class can't have a substitution")
}
- // It's a native-substitution.
+ // It's a redirection class.
val toClass = fields[2].substring(1)
- imf.setNativeSubstitutionClass(className, toClass)
+ imf.setRedirectionClass(className, toClass)
} else if (fields[2].startsWith("~")) {
if (classType != SpecialClass.NotSpecial) {
// We could support it, but not needed at least for now.
@@ -350,6 +350,7 @@
"r", "remove" -> FilterPolicy.Remove
"kc", "keepclass" -> FilterPolicy.KeepClass
"i", "ignore" -> FilterPolicy.Ignore
+ "rdr", "redirect" -> FilterPolicy.Redirect
else -> {
if (s.startsWith("@")) {
FilterPolicy.Substitute
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 41ba928..261ef59c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -21,8 +21,6 @@
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.UnifiedVisitor
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
-import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage
-import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
@@ -57,7 +55,7 @@
protected lateinit var currentPackageName: String
protected lateinit var currentClassName: String
- protected var nativeSubstitutionClass: String? = null
+ protected var redirectionClass: String? = null
protected lateinit var classPolicy: FilterPolicyWithReason
override fun visit(
@@ -72,34 +70,13 @@
currentClassName = name
currentPackageName = getPackageNameFromFullClassName(name)
classPolicy = filter.getPolicyForClass(currentClassName)
+ redirectionClass = filter.getRedirectionClass(currentClassName)
log.d("[%s] visit: %s (package: %s)", this.javaClass.simpleName, name, currentPackageName)
log.indent()
log.v("Emitting class: %s", name)
log.indent()
- filter.getNativeSubstitutionClass(currentClassName)?.let { className ->
- val fullClassName = resolveClassNameWithDefaultPackage(className, currentPackageName)
- .toJvmClassName()
- log.d(" NativeSubstitutionClass: $fullClassName")
- if (classes.findClass(fullClassName) == null) {
- log.w(
- "Native substitution class $fullClassName not found. Class must be " +
- "available at runtime."
- )
- } else {
- // If the class exists, it must have a KeepClass policy.
- if (filter.getPolicyForClass(fullClassName).policy != FilterPolicy.KeepClass) {
- // TODO: Use real annotation name.
- options.errors.onErrorFound(
- "Native substitution class $fullClassName should have @Keep."
- )
- }
- }
-
- nativeSubstitutionClass = fullClassName
- }
-
// Inject annotations to generated classes.
UnifiedVisitor.on(this).visitAnnotation(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, true)
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 057d653..567a69e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -184,9 +184,12 @@
return IgnoreMethodAdapter(descriptor, forceCreateBody, innerVisitor)
.withAnnotation(HostStubGenProcessedAsIgnore.CLASS_DESCRIPTOR)
}
- FilterPolicy.NativeSubstitute -> {
- log.v("Rewriting native method...")
- return NativeSubstitutingMethodAdapter(access, name, descriptor, innerVisitor)
+ FilterPolicy.Redirect -> {
+ log.v("Redirecting method...")
+ return RedirectMethodAdapter(
+ access, name, descriptor,
+ forceCreateBody, innerVisitor
+ )
.withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR)
}
else -> {}
@@ -274,15 +277,16 @@
}
/**
- * A method adapter that rewrite a native method body with a
- * call to a method in the "native substitution" class.
+ * A method adapter that rewrite a method body with a
+ * call to a method in the redirection class.
*/
- private inner class NativeSubstitutingMethodAdapter(
+ private inner class RedirectMethodAdapter(
access: Int,
private val name: String,
private val descriptor: String,
+ createBody: Boolean,
next: MethodVisitor?
- ) : BodyReplacingMethodVisitor(true, next) {
+ ) : BodyReplacingMethodVisitor(createBody, next) {
private val isStatic = (access and Opcodes.ACC_STATIC) != 0
@@ -290,7 +294,7 @@
var targetDescriptor = descriptor
var argOffset = 0
- // For non-static native method, we need to tweak it a bit.
+ // For non-static method, we need to tweak it a bit.
if (!isStatic) {
// Push `this` as the first argument.
this.visitVarInsn(Opcodes.ALOAD, 0)
@@ -310,7 +314,7 @@
visitMethodInsn(
INVOKESTATIC,
- nativeSubstitutionClass,
+ redirectionClass,
name,
targetDescriptor,
false
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 5fde14f..82586bb 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -41,20 +41,40 @@
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.CLASS
)
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
- Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+ Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
minor version: 0
major version: 61
flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
- this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirect
+ super_class: #x // java/lang/Object
+ interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+ x: #x(#x=[e#x.#x])
+ java.lang.annotation.Target(
+ value=[Ljava/lang/annotation/ElementType;.METHOD]
+ )
+ x: #x(#x=e#x.#x)
+ java.lang.annotation.Retention(
+ value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+ )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+ Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+ minor version: 0
+ major version: 61
+ flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass
super_class: #x // java/lang/Object
interfaces: 1, fields: 0, methods: 1, attributes: 2
public abstract java.lang.String value();
descriptor: ()Ljava/lang/String;
flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
}
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
RuntimeVisibleAnnotations:
x: #x(#x=[e#x.#x])
java.lang.annotation.Target(
@@ -1925,7 +1945,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 1, methods: 12, attributes: 2
+ interfaces: 0, fields: 1, methods: 14, attributes: 2
int value;
descriptor: I
flags: (0x0000)
@@ -1946,6 +1966,9 @@
public static native int nativeAddTwo(int);
descriptor: (I)I
flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static int nativeAddTwo_should_be_like_this(int);
descriptor: (I)I
@@ -1963,6 +1986,9 @@
public static native long nativeLongPlus(long, long);
descriptor: (JJ)J
flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static long nativeLongPlus_should_be_like_this(long, long);
descriptor: (JJ)J
@@ -1997,6 +2023,9 @@
public native int nativeNonStaticAddToValue(int);
descriptor: (I)I
flags: (0x0101) ACC_PUBLIC, ACC_NATIVE
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public int nativeNonStaticAddToValue_should_be_like_this(int);
descriptor: (I)I
@@ -2023,9 +2052,6 @@
public static native void nativeStillKeep();
descriptor: ()V
flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
- RuntimeInvisibleAnnotations:
- x: #x()
- android.hosttest.annotation.HostSideTestKeep
public static void nativeStillNotSupported_should_be_like_this();
descriptor: ()V
@@ -2041,13 +2067,47 @@
public static native byte nativeBytePlus(byte, byte);
descriptor: (BB)B
flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public void notNativeRedirected();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=1, args_size=1
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":()V
+ x: athrow
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 8 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=0, args_size=0
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":()V
+ x: athrow
+ LineNumberTable:
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
}
SourceFile: "TinyFrameworkNative.java"
RuntimeInvisibleAnnotations:
x: #x()
android.hosttest.annotation.HostSideTestWholeClassKeep
x: #x(#x=s#x)
- android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+ android.hosttest.annotation.HostSideTestRedirectionClass(
value="TinyFrameworkNative_host"
)
## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2058,7 +2118,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 2
+ interfaces: 0, fields: 0, methods: 7, attributes: 2
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -2132,6 +2192,25 @@
Start Length Slot Name Signature
0 5 0 arg1 B
0 5 1 arg2 B
+
+ public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=0, locals=1, args_size=1
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=0, locals=0, args_size=0
+ x: return
+ LineNumberTable:
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeInvisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
index e41d46d..31bbcc5 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
@@ -48,13 +48,35 @@
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.CLASS
)
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
- Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+ Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
minor version: 0
major version: 61
flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
- this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirect
+ super_class: #x // java/lang/Object
+ interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ x: #x(#x=[e#x.#x])
+ java.lang.annotation.Target(
+ value=[Ljava/lang/annotation/ElementType;.METHOD]
+ )
+ x: #x(#x=e#x.#x)
+ java.lang.annotation.Retention(
+ value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+ )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+ Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+ minor version: 0
+ major version: 61
+ flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass
super_class: #x // java/lang/Object
interfaces: 1, fields: 0, methods: 1, attributes: 2
public abstract java.lang.String value();
@@ -64,7 +86,7 @@
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
}
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
@@ -2076,7 +2098,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 1, methods: 12, attributes: 3
+ interfaces: 0, fields: 1, methods: 14, attributes: 3
int value;
descriptor: I
flags: (0x0000)
@@ -2113,6 +2135,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static int nativeAddTwo_should_be_like_this(int);
descriptor: (I)I
@@ -2144,6 +2169,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static long nativeLongPlus_should_be_like_this(long, long);
descriptor: (JJ)J
@@ -2195,6 +2223,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public int nativeNonStaticAddToValue_should_be_like_this(int);
descriptor: (I)I
@@ -2240,9 +2271,6 @@
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
- RuntimeInvisibleAnnotations:
- x: #x()
- android.hosttest.annotation.HostSideTestKeep
public static void nativeStillNotSupported_should_be_like_this();
descriptor: ()V
@@ -2272,6 +2300,42 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public void notNativeRedirected();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=1, locals=1, args_size=1
+ x: aload_0
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ x: return
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=0, locals=0, args_size=0
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V
+ x: return
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
@@ -2281,7 +2345,7 @@
x: #x()
android.hosttest.annotation.HostSideTestWholeClassKeep
x: #x(#x=s#x)
- android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+ android.hosttest.annotation.HostSideTestRedirectionClass(
value="TinyFrameworkNative_host"
)
## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2292,7 +2356,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 3
+ interfaces: 0, fields: 0, methods: 7, attributes: 3
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -2381,6 +2445,31 @@
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+ public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=0, locals=1, args_size=1
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=0, locals=0, args_size=0
+ x: return
+ LineNumberTable:
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 2ca723b..41f459a 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -67,13 +67,44 @@
java.lang.annotation.Retention(
value=Ljava/lang/annotation/RetentionPolicy;.CLASS
)
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
- Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+ Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
minor version: 0
major version: 61
flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
- this_class: #x // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirect
+ super_class: #x // java/lang/Object
+ interfaces: 1, fields: 0, methods: 1, attributes: 2
+ private static {};
+ descriptor: ()V
+ flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+ Code:
+ stack=2, locals=0, args_size=0
+ x: ldc #x // class android/hosttest/annotation/HostSideTestRedirect
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+ x: return
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ x: #x(#x=[e#x.#x])
+ java.lang.annotation.Target(
+ value=[Ljava/lang/annotation/ElementType;.METHOD]
+ )
+ x: #x(#x=e#x.#x)
+ java.lang.annotation.Retention(
+ value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+ )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+ Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+ minor version: 0
+ major version: 61
+ flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+ this_class: #x // android/hosttest/annotation/HostSideTestRedirectionClass
super_class: #x // java/lang/Object
interfaces: 1, fields: 0, methods: 2, attributes: 2
private static {};
@@ -81,7 +112,7 @@
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
- x: ldc #x // class android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+ x: ldc #x // class android/hosttest/annotation/HostSideTestRedirectionClass
x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
x: return
@@ -93,7 +124,7 @@
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
}
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
@@ -2612,7 +2643,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 1, methods: 13, attributes: 3
+ interfaces: 0, fields: 1, methods: 15, attributes: 3
int value;
descriptor: I
flags: (0x0000)
@@ -2669,6 +2700,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static int nativeAddTwo_should_be_like_this(int);
descriptor: (I)I
@@ -2710,6 +2744,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public static long nativeLongPlus_should_be_like_this(long, long);
descriptor: (JJ)J
@@ -2776,6 +2813,9 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
public int nativeNonStaticAddToValue_should_be_like_this(int);
descriptor: (I)I
@@ -2831,9 +2871,6 @@
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
- RuntimeInvisibleAnnotations:
- x: #x()
- android.hosttest.annotation.HostSideTestKeep
public static void nativeStillNotSupported_should_be_like_this();
descriptor: ()V
@@ -2873,6 +2910,52 @@
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public void notNativeRedirected();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=4, locals=1, args_size=1
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+ x: ldc #x // String notNativeRedirected
+ x: ldc #x // String ()V
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: aload_0
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ x: return
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=4, locals=0, args_size=0
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+ x: ldc #x // String notNativeStaticRedirected
+ x: ldc #x // String ()V
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V
+ x: return
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+ RuntimeInvisibleAnnotations:
+ x: #x()
+ android.hosttest.annotation.HostSideTestRedirect
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
@@ -2882,7 +2965,7 @@
x: #x()
android.hosttest.annotation.HostSideTestWholeClassKeep
x: #x(#x=s#x)
- android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+ android.hosttest.annotation.HostSideTestRedirectionClass(
value="TinyFrameworkNative_host"
)
## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2893,7 +2976,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 6, attributes: 3
+ interfaces: 0, fields: 0, methods: 8, attributes: 3
private static {};
descriptor: ()V
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3017,6 +3100,41 @@
RuntimeVisibleAnnotations:
x: #x()
com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+ public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=4, locals=1, args_size=1
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ x: ldc #x // String notNativeRedirected
+ x: ldc #x // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 11 1 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+ public static void notNativeStaticRedirected();
+ descriptor: ()V
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=4, locals=0, args_size=0
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ x: ldc #x // String notNativeStaticRedirected
+ x: ldc #x // String ()V
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: return
+ LineNumberTable:
+ RuntimeVisibleAnnotations:
+ x: #x()
+ com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
index 73b5e2f..04a551c 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
@@ -15,20 +15,23 @@
*/
package com.android.hoststubgen.test.tinyframework;
-import android.hosttest.annotation.HostSideTestKeep;
-import android.hosttest.annotation.HostSideTestNativeSubstitutionClass;
+import android.hosttest.annotation.HostSideTestRedirect;
+import android.hosttest.annotation.HostSideTestRedirectionClass;
import android.hosttest.annotation.HostSideTestThrow;
import android.hosttest.annotation.HostSideTestWholeClassKeep;
@HostSideTestWholeClassKeep
-@HostSideTestNativeSubstitutionClass("TinyFrameworkNative_host")
+@HostSideTestRedirectionClass("TinyFrameworkNative_host")
public class TinyFrameworkNative {
+
+ @HostSideTestRedirect
public static native int nativeAddTwo(int arg);
public static int nativeAddTwo_should_be_like_this(int arg) {
return TinyFrameworkNative_host.nativeAddTwo(arg);
}
+ @HostSideTestRedirect
public static native long nativeLongPlus(long arg1, long arg2);
public static long nativeLongPlus_should_be_like_this(long arg1, long arg2) {
@@ -41,6 +44,7 @@
this.value = v;
}
+ @HostSideTestRedirect
public native int nativeNonStaticAddToValue(int arg);
public int nativeNonStaticAddToValue_should_be_like_this(int arg) {
@@ -50,12 +54,22 @@
@HostSideTestThrow
public static native void nativeStillNotSupported();
- @HostSideTestKeep
public static native void nativeStillKeep();
public static void nativeStillNotSupported_should_be_like_this() {
throw new RuntimeException();
}
+ @HostSideTestRedirect
public static native byte nativeBytePlus(byte arg1, byte arg2);
+
+ @HostSideTestRedirect
+ public void notNativeRedirected() {
+ throw new RuntimeException();
+ }
+
+ @HostSideTestRedirect
+ public static void notNativeStaticRedirected() {
+ throw new RuntimeException();
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
index b23c216..c7a29a1 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
@@ -17,8 +17,6 @@
import android.hosttest.annotation.HostSideTestWholeClassKeep;
-// TODO: This annotation shouldn't be needed.
-// We should infer it from HostSideTestNativeSubstitutionClass.
@HostSideTestWholeClassKeep
public class TinyFrameworkNative_host {
public static int nativeAddTwo(int arg) {
@@ -38,4 +36,10 @@
public static byte nativeBytePlus(byte arg1, byte arg2) {
return (byte) (arg1 + arg2);
}
+
+ public static void notNativeRedirected(TinyFrameworkNative source) {
+ }
+
+ public static void notNativeStaticRedirected() {
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index 14229a0..68673dc 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -164,6 +164,12 @@
}
@Test
+ public void testNotNativeRedirect() {
+ TinyFrameworkNative.notNativeStaticRedirected();
+ new TinyFrameworkNative().notNativeRedirected();
+ }
+
+ @Test
public void testExitLog() {
thrown.expect(RuntimeException.class);
thrown.expectMessage("Outer exception");
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
index 24d203f..f5af99e 100644
--- a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -24,20 +24,31 @@
import org.jetbrains.uast.UMethod
/**
- * Given a UMethod, determine if this method is the entrypoint to an interface
- * generated by AIDL, returning the interface name if so, otherwise returning
- * null
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the interface name if so, otherwise returning null.
*/
fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
+ return containingAidlInterfacePsiClass(context, node)?.name
+}
+
+/**
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the fully qualified interface name if so, otherwise returning null.
+ */
+fun getContainingAidlInterfaceQualified(context: JavaContext, node: UMethod): String? {
+ return containingAidlInterfacePsiClass(context, node)?.qualifiedName
+}
+
+private fun containingAidlInterfacePsiClass(context: JavaContext, node: UMethod): PsiClass? {
val containingStub = containingStub(context, node) ?: return null
val superMethod = node.findSuperMethods(containingStub)
if (superMethod.isEmpty()) return null
- return containingStub.containingClass?.name
+ return containingStub.containingClass
}
-/* Returns the containing Stub class if any. This is not sufficient to infer
- * that the method itself extends an AIDL generated method. See
- * getContainingAidlInterface for that purpose.
+/**
+ * Returns the containing Stub class if any. This is not sufficient to infer that the method itself
+ * extends an AIDL generated method. See getContainingAidlInterface for that purpose.
*/
fun containingStub(context: JavaContext, node: UMethod?): PsiClass? {
var superClass = node?.containingClass?.superClass
@@ -48,7 +59,7 @@
return null
}
-private fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
+fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
if (psiClass == null) return false
if (psiClass.name != "Stub") return false
if (!context.evaluator.isStatic(psiClass)) return false
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
new file mode 100644
index 0000000..8777712
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
@@ -0,0 +1,774 @@
+/*
+ * 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.google.android.lint.aidl
+
+/**
+ * The exemptAidlInterfaces set was generated by running ExemptAidlInterfacesGenerator on the
+ * entire source tree. To reproduce the results, run generate-exempt-aidl-interfaces.sh
+ * located in tools/lint/utils.
+ *
+ * TODO: b/363248121 - Use the exemptAidlInterfaces set inside PermissionAnnotationDetector when it
+ * gets migrated to a global lint check.
+ */
+val exemptAidlInterfaces = setOf(
+ "android.accessibilityservice.IAccessibilityServiceConnection",
+ "android.accessibilityservice.IBrailleDisplayConnection",
+ "android.accounts.IAccountAuthenticatorResponse",
+ "android.accounts.IAccountManager",
+ "android.accounts.IAccountManagerResponse",
+ "android.adservices.adid.IAdIdProviderService",
+ "android.adservices.adid.IAdIdService",
+ "android.adservices.adid.IGetAdIdCallback",
+ "android.adservices.adid.IGetAdIdProviderCallback",
+ "android.adservices.adselection.AdSelectionCallback",
+ "android.adservices.adselection.AdSelectionOverrideCallback",
+ "android.adservices.adselection.AdSelectionService",
+ "android.adservices.adselection.GetAdSelectionDataCallback",
+ "android.adservices.adselection.PersistAdSelectionResultCallback",
+ "android.adservices.adselection.ReportImpressionCallback",
+ "android.adservices.adselection.ReportInteractionCallback",
+ "android.adservices.adselection.SetAppInstallAdvertisersCallback",
+ "android.adservices.adselection.UpdateAdCounterHistogramCallback",
+ "android.adservices.appsetid.IAppSetIdProviderService",
+ "android.adservices.appsetid.IAppSetIdService",
+ "android.adservices.appsetid.IGetAppSetIdCallback",
+ "android.adservices.appsetid.IGetAppSetIdProviderCallback",
+ "android.adservices.cobalt.IAdServicesCobaltUploadService",
+ "android.adservices.common.IAdServicesCommonCallback",
+ "android.adservices.common.IAdServicesCommonService",
+ "android.adservices.common.IAdServicesCommonStatesCallback",
+ "android.adservices.common.IEnableAdServicesCallback",
+ "android.adservices.common.IUpdateAdIdCallback",
+ "android.adservices.customaudience.CustomAudienceOverrideCallback",
+ "android.adservices.customaudience.FetchAndJoinCustomAudienceCallback",
+ "android.adservices.customaudience.ICustomAudienceCallback",
+ "android.adservices.customaudience.ICustomAudienceService",
+ "android.adservices.customaudience.ScheduleCustomAudienceUpdateCallback",
+ "android.adservices.extdata.IAdServicesExtDataStorageService",
+ "android.adservices.extdata.IGetAdServicesExtDataCallback",
+ "android.adservices.measurement.IMeasurementApiStatusCallback",
+ "android.adservices.measurement.IMeasurementCallback",
+ "android.adservices.measurement.IMeasurementService",
+ "android.adservices.ondevicepersonalization.aidl.IDataAccessService",
+ "android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IExecuteCallback",
+ "android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback",
+ "android.adservices.ondevicepersonalization.aidl.IFederatedComputeService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedModelService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedModelServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedService",
+ "android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationDebugService",
+ "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService",
+ "android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback",
+ "android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback",
+ "android.adservices.shell.IShellCommand",
+ "android.adservices.shell.IShellCommandCallback",
+ "android.adservices.signals.IProtectedSignalsService",
+ "android.adservices.signals.UpdateSignalsCallback",
+ "android.adservices.topics.IGetTopicsCallback",
+ "android.adservices.topics.ITopicsService",
+ "android.app.admin.IDevicePolicyManager",
+ "android.app.adservices.IAdServicesManager",
+ "android.app.ambientcontext.IAmbientContextManager",
+ "android.app.ambientcontext.IAmbientContextObserver",
+ "android.app.appsearch.aidl.IAppFunctionService",
+ "android.app.appsearch.aidl.IAppSearchBatchResultCallback",
+ "android.app.appsearch.aidl.IAppSearchManager",
+ "android.app.appsearch.aidl.IAppSearchObserverProxy",
+ "android.app.appsearch.aidl.IAppSearchResultCallback",
+ "android.app.backup.IBackupCallback",
+ "android.app.backup.IBackupManager",
+ "android.app.backup.IRestoreSession",
+ "android.app.blob.IBlobCommitCallback",
+ "android.app.blob.IBlobStoreManager",
+ "android.app.blob.IBlobStoreSession",
+ "android.app.contentsuggestions.IContentSuggestionsManager",
+ "android.app.contextualsearch.IContextualSearchManager",
+ "android.app.ecm.IEnhancedConfirmationManager",
+ "android.apphibernation.IAppHibernationService",
+ "android.app.IActivityClientController",
+ "android.app.IActivityController",
+ "android.app.IActivityTaskManager",
+ "android.app.IAlarmCompleteListener",
+ "android.app.IAlarmListener",
+ "android.app.IAlarmManager",
+ "android.app.IApplicationThread",
+ "android.app.IAppTask",
+ "android.app.IAppTraceRetriever",
+ "android.app.IAssistDataReceiver",
+ "android.app.IForegroundServiceObserver",
+ "android.app.IGameManagerService",
+ "android.app.IGrammaticalInflectionManager",
+ "android.app.ILocaleManager",
+ "android.app.INotificationManager",
+ "android.app.IParcelFileDescriptorRetriever",
+ "android.app.IProcessObserver",
+ "android.app.ISearchManager",
+ "android.app.IStopUserCallback",
+ "android.app.ITaskStackListener",
+ "android.app.IUiModeManager",
+ "android.app.IUriGrantsManager",
+ "android.app.IUserSwitchObserver",
+ "android.app.IWallpaperManager",
+ "android.app.job.IJobCallback",
+ "android.app.job.IJobScheduler",
+ "android.app.job.IJobService",
+ "android.app.ondeviceintelligence.IDownloadCallback",
+ "android.app.ondeviceintelligence.IFeatureCallback",
+ "android.app.ondeviceintelligence.IFeatureDetailsCallback",
+ "android.app.ondeviceintelligence.IListFeaturesCallback",
+ "android.app.ondeviceintelligence.IOnDeviceIntelligenceManager",
+ "android.app.ondeviceintelligence.IProcessingSignal",
+ "android.app.ondeviceintelligence.IResponseCallback",
+ "android.app.ondeviceintelligence.IStreamingResponseCallback",
+ "android.app.ondeviceintelligence.ITokenInfoCallback",
+ "android.app.people.IPeopleManager",
+ "android.app.pinner.IPinnerService",
+ "android.app.prediction.IPredictionManager",
+ "android.app.role.IOnRoleHoldersChangedListener",
+ "android.app.role.IRoleController",
+ "android.app.role.IRoleManager",
+ "android.app.sdksandbox.ILoadSdkCallback",
+ "android.app.sdksandbox.IRequestSurfacePackageCallback",
+ "android.app.sdksandbox.ISdkSandboxManager",
+ "android.app.sdksandbox.ISdkSandboxProcessDeathCallback",
+ "android.app.sdksandbox.ISdkToServiceCallback",
+ "android.app.sdksandbox.ISharedPreferencesSyncCallback",
+ "android.app.sdksandbox.IUnloadSdkCallback",
+ "android.app.sdksandbox.testutils.testscenario.ISdkSandboxTestExecutor",
+ "android.app.search.ISearchUiManager",
+ "android.app.slice.ISliceManager",
+ "android.app.smartspace.ISmartspaceManager",
+ "android.app.timedetector.ITimeDetectorService",
+ "android.app.timezonedetector.ITimeZoneDetectorService",
+ "android.app.trust.ITrustManager",
+ "android.app.usage.IStorageStatsManager",
+ "android.app.usage.IUsageStatsManager",
+ "android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager",
+ "android.app.wearable.IWearableSensingCallback",
+ "android.app.wearable.IWearableSensingManager",
+ "android.bluetooth.IBluetooth",
+ "android.bluetooth.IBluetoothA2dp",
+ "android.bluetooth.IBluetoothA2dpSink",
+ "android.bluetooth.IBluetoothActivityEnergyInfoListener",
+ "android.bluetooth.IBluetoothAvrcpController",
+ "android.bluetooth.IBluetoothCallback",
+ "android.bluetooth.IBluetoothConnectionCallback",
+ "android.bluetooth.IBluetoothCsipSetCoordinator",
+ "android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback",
+ "android.bluetooth.IBluetoothGatt",
+ "android.bluetooth.IBluetoothGattCallback",
+ "android.bluetooth.IBluetoothGattServerCallback",
+ "android.bluetooth.IBluetoothHapClient",
+ "android.bluetooth.IBluetoothHapClientCallback",
+ "android.bluetooth.IBluetoothHeadset",
+ "android.bluetooth.IBluetoothHeadsetClient",
+ "android.bluetooth.IBluetoothHearingAid",
+ "android.bluetooth.IBluetoothHidDevice",
+ "android.bluetooth.IBluetoothHidDeviceCallback",
+ "android.bluetooth.IBluetoothHidHost",
+ "android.bluetooth.IBluetoothLeAudio",
+ "android.bluetooth.IBluetoothLeAudioCallback",
+ "android.bluetooth.IBluetoothLeBroadcastAssistant",
+ "android.bluetooth.IBluetoothLeBroadcastAssistantCallback",
+ "android.bluetooth.IBluetoothLeBroadcastCallback",
+ "android.bluetooth.IBluetoothLeCallControl",
+ "android.bluetooth.IBluetoothLeCallControlCallback",
+ "android.bluetooth.IBluetoothManager",
+ "android.bluetooth.IBluetoothManagerCallback",
+ "android.bluetooth.IBluetoothMap",
+ "android.bluetooth.IBluetoothMapClient",
+ "android.bluetooth.IBluetoothMcpServiceManager",
+ "android.bluetooth.IBluetoothMetadataListener",
+ "android.bluetooth.IBluetoothOobDataCallback",
+ "android.bluetooth.IBluetoothPan",
+ "android.bluetooth.IBluetoothPanCallback",
+ "android.bluetooth.IBluetoothPbap",
+ "android.bluetooth.IBluetoothPbapClient",
+ "android.bluetooth.IBluetoothPreferredAudioProfilesCallback",
+ "android.bluetooth.IBluetoothQualityReportReadyCallback",
+ "android.bluetooth.IBluetoothSap",
+ "android.bluetooth.IBluetoothScan",
+ "android.bluetooth.IBluetoothSocketManager",
+ "android.bluetooth.IBluetoothVolumeControl",
+ "android.bluetooth.IBluetoothVolumeControlCallback",
+ "android.bluetooth.le.IAdvertisingSetCallback",
+ "android.bluetooth.le.IDistanceMeasurementCallback",
+ "android.bluetooth.le.IPeriodicAdvertisingCallback",
+ "android.bluetooth.le.IScannerCallback",
+ "android.companion.ICompanionDeviceManager",
+ "android.companion.IOnMessageReceivedListener",
+ "android.companion.IOnTransportsChangedListener",
+ "android.companion.virtualcamera.IVirtualCameraCallback",
+ "android.companion.virtual.IVirtualDevice",
+ "android.companion.virtual.IVirtualDeviceManager",
+ "android.companion.virtualnative.IVirtualDeviceManagerNative",
+ "android.content.IClipboard",
+ "android.content.IContentService",
+ "android.content.IIntentReceiver",
+ "android.content.IIntentSender",
+ "android.content.integrity.IAppIntegrityManager",
+ "android.content.IRestrictionsManager",
+ "android.content.ISyncAdapterUnsyncableAccountCallback",
+ "android.content.ISyncContext",
+ "android.content.om.IOverlayManager",
+ "android.content.pm.dex.IArtManager",
+ "android.content.pm.dex.ISnapshotRuntimeProfileCallback",
+ "android.content.pm.IBackgroundInstallControlService",
+ "android.content.pm.ICrossProfileApps",
+ "android.content.pm.IDataLoaderManager",
+ "android.content.pm.IDataLoaderStatusListener",
+ "android.content.pm.ILauncherApps",
+ "android.content.pm.IOnChecksumsReadyListener",
+ "android.content.pm.IOtaDexopt",
+ "android.content.pm.IPackageDataObserver",
+ "android.content.pm.IPackageDeleteObserver",
+ "android.content.pm.IPackageInstaller",
+ "android.content.pm.IPackageInstallerSession",
+ "android.content.pm.IPackageInstallerSessionFileSystemConnector",
+ "android.content.pm.IPackageInstallObserver2",
+ "android.content.pm.IPackageLoadingProgressCallback",
+ "android.content.pm.IPackageManager",
+ "android.content.pm.IPackageManagerNative",
+ "android.content.pm.IPackageMoveObserver",
+ "android.content.pm.IPinItemRequest",
+ "android.content.pm.IShortcutService",
+ "android.content.pm.IStagedApexObserver",
+ "android.content.pm.verify.domain.IDomainVerificationManager",
+ "android.content.res.IResourcesManager",
+ "android.content.rollback.IRollbackManager",
+ "android.credentials.ICredentialManager",
+ "android.debug.IAdbTransport",
+ "android.devicelock.IDeviceLockService",
+ "android.devicelock.IGetDeviceIdCallback",
+ "android.devicelock.IGetKioskAppsCallback",
+ "android.devicelock.IIsDeviceLockedCallback",
+ "android.devicelock.IVoidResultCallback",
+ "android.federatedcompute.aidl.IExampleStoreCallback",
+ "android.federatedcompute.aidl.IExampleStoreIterator",
+ "android.federatedcompute.aidl.IExampleStoreIteratorCallback",
+ "android.federatedcompute.aidl.IExampleStoreService",
+ "android.federatedcompute.aidl.IFederatedComputeCallback",
+ "android.federatedcompute.aidl.IFederatedComputeService",
+ "android.federatedcompute.aidl.IResultHandlingService",
+ "android.flags.IFeatureFlags",
+ "android.frameworks.location.altitude.IAltitudeService",
+ "android.frameworks.vibrator.IVibratorController",
+ "android.frameworks.vibrator.IVibratorControlService",
+ "android.gsi.IGsiServiceCallback",
+ "android.hardware.biometrics.AuthenticationStateListener",
+ "android.hardware.biometrics.common.ICancellationSignal",
+ "android.hardware.biometrics.face.IFace",
+ "android.hardware.biometrics.face.ISession",
+ "android.hardware.biometrics.face.ISessionCallback",
+ "android.hardware.biometrics.fingerprint.IFingerprint",
+ "android.hardware.biometrics.fingerprint.ISession",
+ "android.hardware.biometrics.fingerprint.ISessionCallback",
+ "android.hardware.biometrics.IAuthService",
+ "android.hardware.biometrics.IBiometricAuthenticator",
+ "android.hardware.biometrics.IBiometricContextListener",
+ "android.hardware.biometrics.IBiometricSensorReceiver",
+ "android.hardware.biometrics.IBiometricService",
+ "android.hardware.biometrics.IBiometricStateListener",
+ "android.hardware.biometrics.IBiometricSysuiReceiver",
+ "android.hardware.biometrics.IInvalidationCallback",
+ "android.hardware.biometrics.ITestSession",
+ "android.hardware.broadcastradio.IAnnouncementListener",
+ "android.hardware.broadcastradio.ITunerCallback",
+ "android.hardware.contexthub.IContextHubCallback",
+ "android.hardware.devicestate.IDeviceStateManager",
+ "android.hardware.display.IColorDisplayManager",
+ "android.hardware.display.IDisplayManager",
+ "android.hardware.face.IFaceAuthenticatorsRegisteredCallback",
+ "android.hardware.face.IFaceService",
+ "android.hardware.face.IFaceServiceReceiver",
+ "android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback",
+ "android.hardware.fingerprint.IFingerprintClientActiveCallback",
+ "android.hardware.fingerprint.IFingerprintService",
+ "android.hardware.fingerprint.IFingerprintServiceReceiver",
+ "android.hardware.fingerprint.IUdfpsOverlayControllerCallback",
+ "android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback",
+ "android.hardware.hdmi.IHdmiControlCallback",
+ "android.hardware.hdmi.IHdmiControlService",
+ "android.hardware.hdmi.IHdmiDeviceEventListener",
+ "android.hardware.hdmi.IHdmiHotplugEventListener",
+ "android.hardware.hdmi.IHdmiSystemAudioModeChangeListener",
+ "android.hardware.health.IHealthInfoCallback",
+ "android.hardware.ICameraServiceProxy",
+ "android.hardware.IConsumerIrService",
+ "android.hardware.input.IInputManager",
+ "android.hardware.iris.IIrisService",
+ "android.hardware.ISensorPrivacyManager",
+ "android.hardware.ISerialManager",
+ "android.hardware.lights.ILightsManager",
+ "android.hardware.location.IContextHubClient",
+ "android.hardware.location.IContextHubClientCallback",
+ "android.hardware.location.IContextHubService",
+ "android.hardware.location.IContextHubTransactionCallback",
+ "android.hardware.location.ISignificantPlaceProviderManager",
+ "android.hardware.radio.IAnnouncementListener",
+ "android.hardware.radio.ICloseHandle",
+ "android.hardware.radio.ims.media.IImsMedia",
+ "android.hardware.radio.ims.media.IImsMediaListener",
+ "android.hardware.radio.ims.media.IImsMediaSession",
+ "android.hardware.radio.ims.media.IImsMediaSessionListener",
+ "android.hardware.radio.IRadioService",
+ "android.hardware.radio.ITuner",
+ "android.hardware.radio.sap.ISapCallback",
+ "android.hardware.soundtrigger3.ISoundTriggerHw",
+ "android.hardware.soundtrigger3.ISoundTriggerHwCallback",
+ "android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback",
+ "android.hardware.soundtrigger.IRecognitionStatusCallback",
+ "android.hardware.tetheroffload.ITetheringOffloadCallback",
+ "android.hardware.thermal.IThermalChangedCallback",
+ "android.hardware.tv.hdmi.cec.IHdmiCecCallback",
+ "android.hardware.tv.hdmi.connection.IHdmiConnectionCallback",
+ "android.hardware.tv.hdmi.earc.IEArcCallback",
+ "android.hardware.usb.gadget.IUsbGadgetCallback",
+ "android.hardware.usb.IUsbCallback",
+ "android.hardware.usb.IUsbManager",
+ "android.hardware.usb.IUsbSerialReader",
+ "android.hardware.wifi.hostapd.IHostapdCallback",
+ "android.hardware.wifi.IWifiChipEventCallback",
+ "android.hardware.wifi.IWifiEventCallback",
+ "android.hardware.wifi.IWifiNanIfaceEventCallback",
+ "android.hardware.wifi.IWifiRttControllerEventCallback",
+ "android.hardware.wifi.IWifiStaIfaceEventCallback",
+ "android.hardware.wifi.supplicant.INonStandardCertCallback",
+ "android.hardware.wifi.supplicant.ISupplicantP2pIfaceCallback",
+ "android.hardware.wifi.supplicant.ISupplicantStaIfaceCallback",
+ "android.hardware.wifi.supplicant.ISupplicantStaNetworkCallback",
+ "android.health.connect.aidl.IAccessLogsResponseCallback",
+ "android.health.connect.aidl.IActivityDatesResponseCallback",
+ "android.health.connect.aidl.IAggregateRecordsResponseCallback",
+ "android.health.connect.aidl.IApplicationInfoResponseCallback",
+ "android.health.connect.aidl.IChangeLogsResponseCallback",
+ "android.health.connect.aidl.IDataStagingFinishedCallback",
+ "android.health.connect.aidl.IEmptyResponseCallback",
+ "android.health.connect.aidl.IGetChangeLogTokenCallback",
+ "android.health.connect.aidl.IGetHealthConnectDataStateCallback",
+ "android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback",
+ "android.health.connect.aidl.IGetPriorityResponseCallback",
+ "android.health.connect.aidl.IHealthConnectService",
+ "android.health.connect.aidl.IInsertRecordsResponseCallback",
+ "android.health.connect.aidl.IMedicalDataSourceResponseCallback",
+ "android.health.connect.aidl.IMedicalResourcesResponseCallback",
+ "android.health.connect.aidl.IMigrationCallback",
+ "android.health.connect.aidl.IReadMedicalResourcesResponseCallback",
+ "android.health.connect.aidl.IReadRecordsResponseCallback",
+ "android.health.connect.aidl.IRecordTypeInfoResponseCallback",
+ "android.health.connect.exportimport.IImportStatusCallback",
+ "android.health.connect.exportimport.IQueryDocumentProvidersCallback",
+ "android.health.connect.exportimport.IScheduledExportStatusCallback",
+ "android.location.ICountryDetector",
+ "android.location.IGpsGeofenceHardware",
+ "android.location.ILocationManager",
+ "android.location.provider.ILocationProviderManager",
+ "android.media.IAudioRoutesObserver",
+ "android.media.IMediaCommunicationService",
+ "android.media.IMediaCommunicationServiceCallback",
+ "android.media.IMediaController2",
+ "android.media.IMediaRoute2ProviderServiceCallback",
+ "android.media.IMediaRouterService",
+ "android.media.IMediaSession2",
+ "android.media.IMediaSession2Service",
+ "android.media.INativeSpatializerCallback",
+ "android.media.IPlaybackConfigDispatcher",
+ "android.media.IRecordingConfigDispatcher",
+ "android.media.IRemoteDisplayCallback",
+ "android.media.ISoundDoseCallback",
+ "android.media.ISpatializerHeadTrackingCallback",
+ "android.media.ITranscodingClientCallback",
+ "android.media.metrics.IMediaMetricsManager",
+ "android.media.midi.IMidiManager",
+ "android.media.musicrecognition.IMusicRecognitionAttributionTagCallback",
+ "android.media.musicrecognition.IMusicRecognitionManager",
+ "android.media.musicrecognition.IMusicRecognitionServiceCallback",
+ "android.media.projection.IMediaProjection",
+ "android.media.projection.IMediaProjectionCallback",
+ "android.media.projection.IMediaProjectionManager",
+ "android.media.projection.IMediaProjectionWatcherCallback",
+ "android.media.session.ISession",
+ "android.media.session.ISessionController",
+ "android.media.session.ISessionManager",
+ "android.media.soundtrigger.ISoundTriggerDetectionServiceClient",
+ "android.media.soundtrigger_middleware.IInjectGlobalEvent",
+ "android.media.soundtrigger_middleware.IInjectModelEvent",
+ "android.media.soundtrigger_middleware.IInjectRecognitionEvent",
+ "android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService",
+ "android.media.soundtrigger_middleware.ISoundTriggerModule",
+ "android.media.tv.ad.ITvAdManager",
+ "android.media.tv.ad.ITvAdSessionCallback",
+ "android.media.tv.interactive.ITvInteractiveAppManager",
+ "android.media.tv.interactive.ITvInteractiveAppServiceCallback",
+ "android.media.tv.interactive.ITvInteractiveAppSessionCallback",
+ "android.media.tv.ITvInputHardware",
+ "android.media.tv.ITvInputManager",
+ "android.media.tv.ITvInputServiceCallback",
+ "android.media.tv.ITvInputSessionCallback",
+ "android.media.tv.ITvRemoteServiceInput",
+ "android.nearby.aidl.IOffloadCallback",
+ "android.nearby.IBroadcastListener",
+ "android.nearby.INearbyManager",
+ "android.nearby.IScanListener",
+ "android.net.connectivity.aidl.ConnectivityNative",
+ "android.net.dhcp.IDhcpEventCallbacks",
+ "android.net.dhcp.IDhcpServer",
+ "android.net.dhcp.IDhcpServerCallbacks",
+ "android.net.ICaptivePortal",
+ "android.net.IConnectivityDiagnosticsCallback",
+ "android.net.IConnectivityManager",
+ "android.net.IEthernetManager",
+ "android.net.IEthernetServiceListener",
+ "android.net.IIntResultListener",
+ "android.net.IIpConnectivityMetrics",
+ "android.net.IIpMemoryStore",
+ "android.net.IIpMemoryStoreCallbacks",
+ "android.net.IIpSecService",
+ "android.net.INetdEventCallback",
+ "android.net.INetdUnsolicitedEventListener",
+ "android.net.INetworkActivityListener",
+ "android.net.INetworkAgent",
+ "android.net.INetworkAgentRegistry",
+ "android.net.INetworkInterfaceOutcomeReceiver",
+ "android.net.INetworkManagementEventObserver",
+ "android.net.INetworkMonitor",
+ "android.net.INetworkMonitorCallbacks",
+ "android.net.INetworkOfferCallback",
+ "android.net.INetworkPolicyListener",
+ "android.net.INetworkPolicyManager",
+ "android.net.INetworkScoreService",
+ "android.net.INetworkStackConnector",
+ "android.net.INetworkStackStatusCallback",
+ "android.net.INetworkStatsService",
+ "android.net.INetworkStatsSession",
+ "android.net.IOnCompleteListener",
+ "android.net.IPacProxyManager",
+ "android.net.ip.IIpClient",
+ "android.net.ip.IIpClientCallbacks",
+ "android.net.ipmemorystore.IOnBlobRetrievedListener",
+ "android.net.ipmemorystore.IOnL2KeyResponseListener",
+ "android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener",
+ "android.net.ipmemorystore.IOnSameL3NetworkResponseListener",
+ "android.net.ipmemorystore.IOnStatusAndCountListener",
+ "android.net.ipmemorystore.IOnStatusListener",
+ "android.net.IQosCallback",
+ "android.net.ISocketKeepaliveCallback",
+ "android.net.ITestNetworkManager",
+ "android.net.ITetheredInterfaceCallback",
+ "android.net.ITetheringConnector",
+ "android.net.ITetheringEventCallback",
+ "android.net.IVpnManager",
+ "android.net.mdns.aidl.IMDnsEventListener",
+ "android.net.metrics.INetdEventListener",
+ "android.net.netstats.IUsageCallback",
+ "android.net.netstats.provider.INetworkStatsProvider",
+ "android.net.netstats.provider.INetworkStatsProviderCallback",
+ "android.net.nsd.INsdManager",
+ "android.net.nsd.INsdManagerCallback",
+ "android.net.nsd.INsdServiceConnector",
+ "android.net.nsd.IOffloadEngine",
+ "android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener",
+ "android.net.thread.IActiveOperationalDatasetReceiver",
+ "android.net.thread.IConfigurationReceiver",
+ "android.net.thread.IOperationalDatasetCallback",
+ "android.net.thread.IOperationReceiver",
+ "android.net.thread.IStateCallback",
+ "android.net.thread.IThreadNetworkController",
+ "android.net.thread.IThreadNetworkManager",
+ "android.net.vcn.IVcnManagementService",
+ "android.net.wear.ICompanionDeviceManagerProxy",
+ "android.net.wifi.aware.IWifiAwareDiscoverySessionCallback",
+ "android.net.wifi.aware.IWifiAwareEventCallback",
+ "android.net.wifi.aware.IWifiAwareMacAddressProvider",
+ "android.net.wifi.aware.IWifiAwareManager",
+ "android.net.wifi.hotspot2.IProvisioningCallback",
+ "android.net.wifi.IActionListener",
+ "android.net.wifi.IBooleanListener",
+ "android.net.wifi.IByteArrayListener",
+ "android.net.wifi.ICoexCallback",
+ "android.net.wifi.IDppCallback",
+ "android.net.wifi.IIntegerListener",
+ "android.net.wifi.IInterfaceCreationInfoCallback",
+ "android.net.wifi.ILastCallerListener",
+ "android.net.wifi.IListListener",
+ "android.net.wifi.ILocalOnlyConnectionStatusListener",
+ "android.net.wifi.ILocalOnlyHotspotCallback",
+ "android.net.wifi.IMacAddressListListener",
+ "android.net.wifi.IMapListener",
+ "android.net.wifi.INetworkRequestMatchCallback",
+ "android.net.wifi.INetworkRequestUserSelectionCallback",
+ "android.net.wifi.IOnWifiActivityEnergyInfoListener",
+ "android.net.wifi.IOnWifiDriverCountryCodeChangedListener",
+ "android.net.wifi.IOnWifiUsabilityStatsListener",
+ "android.net.wifi.IPnoScanResultsCallback",
+ "android.net.wifi.IScanDataListener",
+ "android.net.wifi.IScanResultsCallback",
+ "android.net.wifi.IScoreUpdateObserver",
+ "android.net.wifi.ISoftApCallback",
+ "android.net.wifi.IStringListener",
+ "android.net.wifi.ISubsystemRestartCallback",
+ "android.net.wifi.ISuggestionConnectionStatusListener",
+ "android.net.wifi.ISuggestionUserApprovalStatusListener",
+ "android.net.wifi.ITrafficStateCallback",
+ "android.net.wifi.ITwtCallback",
+ "android.net.wifi.ITwtCapabilitiesListener",
+ "android.net.wifi.ITwtStatsListener",
+ "android.net.wifi.IWifiBandsListener",
+ "android.net.wifi.IWifiConnectedNetworkScorer",
+ "android.net.wifi.IWifiLowLatencyLockListener",
+ "android.net.wifi.IWifiManager",
+ "android.net.wifi.IWifiNetworkSelectionConfigListener",
+ "android.net.wifi.IWifiNetworkStateChangedListener",
+ "android.net.wifi.IWifiScanner",
+ "android.net.wifi.IWifiScannerListener",
+ "android.net.wifi.IWifiVerboseLoggingStatusChangedListener",
+ "android.net.wifi.p2p.IWifiP2pListener",
+ "android.net.wifi.p2p.IWifiP2pManager",
+ "android.net.wifi.rtt.IRttCallback",
+ "android.net.wifi.rtt.IWifiRttManager",
+ "android.ondevicepersonalization.IOnDevicePersonalizationSystemService",
+ "android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback",
+ "android.os.IBatteryPropertiesRegistrar",
+ "android.os.ICancellationSignal",
+ "android.os.IDeviceIdentifiersPolicyService",
+ "android.os.IDeviceIdleController",
+ "android.os.IDumpstate",
+ "android.os.IDumpstateListener",
+ "android.os.IExternalVibratorService",
+ "android.os.IHardwarePropertiesManager",
+ "android.os.IHintManager",
+ "android.os.IHintSession",
+ "android.os.IIncidentCompanion",
+ "android.os.image.IDynamicSystemService",
+ "android.os.incremental.IStorageHealthListener",
+ "android.os.INetworkManagementService",
+ "android.os.IPendingIntentRef",
+ "android.os.IPowerStatsService",
+ "android.os.IProfilingResultCallback",
+ "android.os.IProfilingService",
+ "android.os.IProgressListener",
+ "android.os.IPullAtomCallback",
+ "android.os.IRecoverySystem",
+ "android.os.IRemoteCallback",
+ "android.os.ISecurityStateManager",
+ "android.os.IServiceCallback",
+ "android.os.IStatsCompanionService",
+ "android.os.IStatsManagerService",
+ "android.os.IStatsQueryCallback",
+ "android.os.ISystemConfig",
+ "android.os.ISystemUpdateManager",
+ "android.os.IThermalEventListener",
+ "android.os.IUpdateLock",
+ "android.os.IUserManager",
+ "android.os.IUserRestrictionsListener",
+ "android.os.IVibratorManagerService",
+ "android.os.IVoldListener",
+ "android.os.IVoldMountCallback",
+ "android.os.IVoldTaskListener",
+ "android.os.logcat.ILogcatManagerService",
+ "android.permission.ILegacyPermissionManager",
+ "android.permission.IPermissionChecker",
+ "android.permission.IPermissionManager",
+ "android.print.IPrintManager",
+ "android.print.IPrintSpoolerCallbacks",
+ "android.print.IPrintSpoolerClient",
+ "android.printservice.IPrintServiceClient",
+ "android.printservice.recommendation.IRecommendationServiceCallbacks",
+ "android.provider.aidl.IDeviceConfigManager",
+ "android.remoteauth.IDeviceDiscoveryListener",
+ "android.safetycenter.IOnSafetyCenterDataChangedListener",
+ "android.safetycenter.ISafetyCenterManager",
+ "android.scheduling.IRebootReadinessManager",
+ "android.scheduling.IRequestRebootReadinessStatusListener",
+ "android.security.attestationverification.IAttestationVerificationManagerService",
+ "android.security.IFileIntegrityService",
+ "android.security.keystore.IKeyAttestationApplicationIdProvider",
+ "android.security.rkp.IRegistration",
+ "android.security.rkp.IRemoteProvisioning",
+ "android.service.appprediction.IPredictionService",
+ "android.service.assist.classification.IFieldClassificationCallback",
+ "android.service.attention.IAttentionCallback",
+ "android.service.attention.IProximityUpdateCallback",
+ "android.service.autofill.augmented.IFillCallback",
+ "android.service.autofill.IConvertCredentialCallback",
+ "android.service.autofill.IFillCallback",
+ "android.service.autofill.IInlineSuggestionUiCallback",
+ "android.service.autofill.ISaveCallback",
+ "android.service.autofill.ISurfacePackageResultCallback",
+ "android.service.contentcapture.IContentCaptureServiceCallback",
+ "android.service.contentcapture.IContentProtectionAllowlistCallback",
+ "android.service.contentcapture.IDataShareCallback",
+ "android.service.credentials.IBeginCreateCredentialCallback",
+ "android.service.credentials.IBeginGetCredentialCallback",
+ "android.service.credentials.IClearCredentialStateCallback",
+ "android.service.dreams.IDreamManager",
+ "android.service.games.IGameServiceController",
+ "android.service.games.IGameSessionController",
+ "android.service.notification.IStatusBarNotificationHolder",
+ "android.service.oemlock.IOemLockService",
+ "android.service.ondeviceintelligence.IProcessingUpdateStatusCallback",
+ "android.service.ondeviceintelligence.IRemoteProcessingService",
+ "android.service.ondeviceintelligence.IRemoteStorageService",
+ "android.service.persistentdata.IPersistentDataBlockService",
+ "android.service.resolver.IResolverRankerResult",
+ "android.service.rotationresolver.IRotationResolverCallback",
+ "android.service.textclassifier.ITextClassifierCallback",
+ "android.service.textclassifier.ITextClassifierService",
+ "android.service.timezone.ITimeZoneProviderManager",
+ "android.service.trust.ITrustAgentServiceCallback",
+ "android.service.voice.IDetectorSessionStorageService",
+ "android.service.voice.IDetectorSessionVisualQueryDetectionCallback",
+ "android.service.voice.IDspHotwordDetectionCallback",
+ "android.service.wallpaper.IWallpaperConnection",
+ "android.speech.IRecognitionListener",
+ "android.speech.IRecognitionService",
+ "android.speech.IRecognitionServiceManager",
+ "android.speech.tts.ITextToSpeechManager",
+ "android.speech.tts.ITextToSpeechSession",
+ "android.system.composd.ICompilationTaskCallback",
+ "android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback",
+ "android.system.virtualizationservice.IVirtualMachineCallback",
+ "android.system.vmtethering.IVmTethering",
+ "android.telephony.imsmedia.IImsAudioSession",
+ "android.telephony.imsmedia.IImsAudioSessionCallback",
+ "android.telephony.imsmedia.IImsMedia",
+ "android.telephony.imsmedia.IImsMediaCallback",
+ "android.telephony.imsmedia.IImsTextSession",
+ "android.telephony.imsmedia.IImsTextSessionCallback",
+ "android.telephony.imsmedia.IImsVideoSession",
+ "android.telephony.imsmedia.IImsVideoSessionCallback",
+ "android.tracing.ITracingServiceProxy",
+ "android.uwb.IOnUwbActivityEnergyInfoListener",
+ "android.uwb.IUwbAdapter",
+ "android.uwb.IUwbAdapterStateCallbacks",
+ "android.uwb.IUwbAdfProvisionStateCallbacks",
+ "android.uwb.IUwbOemExtensionCallback",
+ "android.uwb.IUwbRangingCallbacks",
+ "android.uwb.IUwbVendorUciCallback",
+ "android.view.accessibility.IAccessibilityInteractionConnectionCallback",
+ "android.view.accessibility.IAccessibilityManager",
+ "android.view.accessibility.IMagnificationConnectionCallback",
+ "android.view.accessibility.IRemoteMagnificationAnimationCallback",
+ "android.view.autofill.IAutoFillManager",
+ "android.view.autofill.IAutofillWindowPresenter",
+ "android.view.contentcapture.IContentCaptureManager",
+ "android.view.IDisplayChangeWindowCallback",
+ "android.view.IDisplayWindowListener",
+ "android.view.IInputFilter",
+ "android.view.IInputFilterHost",
+ "android.view.IInputMonitorHost",
+ "android.view.IRecentsAnimationController",
+ "android.view.IRemoteAnimationFinishedCallback",
+ "android.view.ISensitiveContentProtectionManager",
+ "android.view.IWindowId",
+ "android.view.IWindowManager",
+ "android.view.IWindowSession",
+ "android.view.translation.ITranslationManager",
+ "android.view.translation.ITranslationServiceCallback",
+ "android.webkit.IWebViewUpdateService",
+ "android.window.IBackAnimationFinishedCallback",
+ "android.window.IDisplayAreaOrganizerController",
+ "android.window.ITaskFragmentOrganizerController",
+ "android.window.ITaskOrganizerController",
+ "android.window.ITransitionMetricsReporter",
+ "android.window.IUnhandledDragCallback",
+ "android.window.IWindowContainerToken",
+ "android.window.IWindowlessStartingSurfaceCallback",
+ "android.window.IWindowOrganizerController",
+ "androidx.core.uwb.backend.IUwb",
+ "androidx.core.uwb.backend.IUwbClient",
+ "com.android.clockwork.modes.IModeManager",
+ "com.android.clockwork.modes.IStateChangeListener",
+ "com.android.clockwork.power.IWearPowerService",
+ "com.android.devicelockcontroller.IDeviceLockControllerService",
+ "com.android.devicelockcontroller.storage.IGlobalParametersService",
+ "com.android.devicelockcontroller.storage.ISetupParametersService",
+ "com.android.federatedcompute.services.training.aidl.IIsolatedTrainingService",
+ "com.android.federatedcompute.services.training.aidl.ITrainingResultCallback",
+ "com.android.internal.app.IAppOpsActiveCallback",
+ "com.android.internal.app.ILogAccessDialogCallback",
+ "com.android.internal.app.ISoundTriggerService",
+ "com.android.internal.app.ISoundTriggerSession",
+ "com.android.internal.app.IVoiceInteractionAccessibilitySettingsListener",
+ "com.android.internal.app.IVoiceInteractionManagerService",
+ "com.android.internal.app.IVoiceInteractionSessionListener",
+ "com.android.internal.app.IVoiceInteractionSessionShowCallback",
+ "com.android.internal.app.IVoiceInteractionSoundTriggerSession",
+ "com.android.internal.app.procstats.IProcessStats",
+ "com.android.internal.appwidget.IAppWidgetService",
+ "com.android.internal.backup.ITransportStatusCallback",
+ "com.android.internal.compat.IOverrideValidator",
+ "com.android.internal.compat.IPlatformCompat",
+ "com.android.internal.compat.IPlatformCompatNative",
+ "com.android.internal.graphics.fonts.IFontManager",
+ "com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback",
+ "com.android.internal.inputmethod.IConnectionlessHandwritingCallback",
+ "com.android.internal.inputmethod.IImeTracker",
+ "com.android.internal.inputmethod.IInlineSuggestionsRequestCallback",
+ "com.android.internal.inputmethod.IInputContentUriToken",
+ "com.android.internal.inputmethod.IInputMethodPrivilegedOperations",
+ "com.android.internal.inputmethod.IInputMethodSessionCallback",
+ "com.android.internal.net.INetworkWatchlistManager",
+ "com.android.internal.os.IBinaryTransparencyService",
+ "com.android.internal.os.IDropBoxManagerService",
+ "com.android.internal.policy.IKeyguardDismissCallback",
+ "com.android.internal.policy.IKeyguardDrawnCallback",
+ "com.android.internal.policy.IKeyguardExitCallback",
+ "com.android.internal.policy.IKeyguardStateCallback",
+ "com.android.internal.statusbar.IAddTileResultCallback",
+ "com.android.internal.statusbar.ISessionListener",
+ "com.android.internal.statusbar.IStatusBarService",
+ "com.android.internal.telecom.IDeviceIdleControllerAdapter",
+ "com.android.internal.telecom.IInternalServiceRetriever",
+ "com.android.internal.telephony.IMms",
+ "com.android.internal.telephony.ITelephonyRegistry",
+ "com.android.internal.textservice.ISpellCheckerServiceCallback",
+ "com.android.internal.textservice.ITextServicesManager",
+ "com.android.internal.view.IDragAndDropPermissions",
+ "com.android.internal.view.IInputMethodManager",
+ "com.android.internal.view.inline.IInlineContentProvider",
+ "com.android.internal.widget.ILockSettings",
+ "com.android.net.IProxyPortListener",
+ "com.android.net.module.util.IRoutingCoordinator",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginCallback",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginExecutorService",
+ "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginStateCallback",
+ "com.android.rkpdapp.IGetKeyCallback",
+ "com.android.rkpdapp.IGetRegistrationCallback",
+ "com.android.rkpdapp.IRegistration",
+ "com.android.rkpdapp.IRemoteProvisioning",
+ "com.android.rkpdapp.IStoreUpgradedKeyCallback",
+ "com.android.sdksandbox.IComputeSdkStorageCallback",
+ "com.android.sdksandbox.ILoadSdkInSandboxCallback",
+ "com.android.sdksandbox.IRequestSurfacePackageFromSdkCallback",
+ "com.android.sdksandbox.ISdkSandboxManagerToSdkSandboxCallback",
+ "com.android.sdksandbox.ISdkSandboxService",
+ "com.android.sdksandbox.IUnloadSdkInSandboxCallback",
+ "com.android.server.profcollect.IProviderStatusCallback",
+ "com.android.server.thread.openthread.IChannelMasksReceiver",
+ "com.android.server.thread.openthread.INsdPublisher",
+ "com.android.server.thread.openthread.IOtDaemonCallback",
+ "com.android.server.thread.openthread.IOtStatusReceiver",
+ "com.google.android.clockwork.ambient.offload.IDisplayOffloadService",
+ "com.google.android.clockwork.ambient.offload.IDisplayOffloadTransitionFinishedCallbacks",
+ "com.google.android.clockwork.healthservices.IHealthService",
+ "vendor.google_clockwork.healthservices.IHealthServicesCallback",
+)
diff --git a/tools/lint/utils/README.md b/tools/lint/utils/README.md
new file mode 100644
index 0000000..b5583c5
--- /dev/null
+++ b/tools/lint/utils/README.md
@@ -0,0 +1,11 @@
+# Utility Android Lint Checks for AOSP
+
+This directory contains scripts that execute utility Android Lint Checks for AOSP, specifically:
+* `enforce_permission_counter.py`: Provides statistics regarding the percentage of annotated/not
+ annotated `AIDL` methods with `@EnforcePermission` annotations.
+* `generate-exempt-aidl-interfaces.sh`: Provides a list of all `AIDL` interfaces in the entire
+ source tree.
+
+When adding a new utility Android Lint check to this directory, consider adding any utility or
+data processing tool you might require. Make sure that your contribution is documented in this
+README file.
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
index fa61c42..9842881 100644
--- a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
@@ -19,6 +19,7 @@
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.client.api.Vendor
import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.aidl.ExemptAidlInterfacesGenerator
import com.google.android.lint.aidl.AnnotatedAidlCounter
import com.google.auto.service.AutoService
@@ -27,6 +28,7 @@
class AndroidUtilsIssueRegistry : IssueRegistry() {
override val issues = listOf(
AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+ ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
)
override val api: Int
@@ -38,6 +40,6 @@
override val vendor: Vendor = Vendor(
vendorName = "Android",
feedbackUrl = "http://b/issues/new?component=315013",
- contact = "tweek@google.com"
+ contact = "android-platform-abuse-prevention-withfriends@google.com"
)
}
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
new file mode 100644
index 0000000..6ad223c
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Generates a set of fully qualified AIDL Interface names present in the entire source tree with
+ * the following requirement: their implementations have to be inside directories whose path
+ * prefixes match `systemServicePathPrefixes`.
+ */
+class ExemptAidlInterfacesGenerator : AidlImplementationDetector() {
+ private val targetExemptAidlInterfaceNames = mutableSetOf<String>()
+ private val systemServicePathPrefixes = setOf(
+ "frameworks/base/services",
+ "frameworks/base/apex",
+ "frameworks/opt/wear",
+ "packages/modules"
+ )
+
+ // We could've improved performance by visiting classes rather than methods, however, this lint
+ // check won't be run regularly, hence we've decided not to add extra overrides to
+ // AidlImplementationDetector.
+ override fun visitAidlMethod(
+ context: JavaContext,
+ node: UMethod,
+ interfaceName: String,
+ body: UBlockExpression
+ ) {
+ val filePath = context.file.path
+
+ // We perform `filePath.contains` instead of `filePath.startsWith` since getting the
+ // relative path of a source file is non-trivial. That is because `context.file.path`
+ // returns the path to where soong builds the file (i.e. /out/soong/...). Moreover, the
+ // logic to extract the relative path would need to consider several /out/soong/...
+ // locations patterns.
+ if (systemServicePathPrefixes.none { filePath.contains(it) }) return
+
+ val fullyQualifiedInterfaceName =
+ getContainingAidlInterfaceQualified(context, node) ?: return
+
+ targetExemptAidlInterfaceNames.add("\"$fullyQualifiedInterfaceName\",")
+ }
+
+ override fun afterCheckEachProject(context: Context) {
+ if (targetExemptAidlInterfaceNames.isEmpty()) return
+
+ val message = targetExemptAidlInterfaceNames.joinToString("\n")
+
+ context.report(
+ ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+ context.getLocation(context.project.dir),
+ "\n" + message + "\n",
+ )
+ }
+
+ companion object {
+ @JvmField
+ val ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES = Issue.create(
+ id = "PermissionAnnotationExemptAidlInterfaces",
+ briefDescription = "Returns a set of all AIDL interfaces",
+ explanation = """
+ Produces the exemptAidlInterfaces set used by PermissionAnnotationDetector
+ """.trimIndent(),
+ category = Category.SECURITY,
+ priority = 5,
+ severity = Severity.INFORMATIONAL,
+ implementation = Implementation(
+ ExemptAidlInterfacesGenerator::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+ }
+}
diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
new file mode 100644
index 0000000..9a17bb4
--- /dev/null
+++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+class ExemptAidlInterfacesGeneratorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = ExemptAidlInterfacesGenerator()
+
+ override fun getIssues(): List<Issue> = listOf(
+ ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+ )
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ fun testMultipleAidlInterfacesImplemented() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass1.java"),
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ java(
+ createVisitedPath("TestClass2.java"),
+ """
+ package com.android.server;
+ public class TestClass2 extends IBar.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo",
+ "IBar", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testSingleAidlInterfaceRepeated() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass1.java"),
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ java(
+ createVisitedPath("TestClass2.java"),
+ """
+ package com.android.server;
+ public class TestClass2 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testAnonymousClassExtendsAidlStub() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass.java"),
+ """
+ package com.android.server;
+ public class TestClass {
+ private IBinder aidlImpl = new IFoo.Stub() {
+ public void testMethod() {}
+ };
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expect(
+ """
+ app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+ 0 errors, 0 warnings
+ """
+ )
+ }
+
+ fun testNoAidlInterfacesImplemented() {
+ lint()
+ .files(
+ java(
+ createVisitedPath("TestClass.java"),
+ """
+ package com.android.server;
+ public class TestClass {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testAidlInterfaceImplementedInIgnoredDirectory() {
+ lint()
+ .files(
+ java(
+ ignoredPath,
+ """
+ package com.android.server;
+ public class TestClass1 extends IFoo.Stub {
+ public void testMethod() {}
+ }
+ """
+ )
+ .indented(),
+ *stubs,
+ )
+ .run()
+ .expectClean()
+ }
+
+ private val interfaceIFoo: TestFile = java(
+ """
+ public interface IFoo extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IFoo {}
+ public void testMethod();
+ }
+ """
+ ).indented()
+
+ private val interfaceIBar: TestFile = java(
+ """
+ public interface IBar extends android.os.IInterface {
+ public static abstract class Stub extends android.os.Binder implements IBar {}
+ public void testMethod();
+ }
+ """
+ ).indented()
+
+ private val stubs = arrayOf(interfaceIFoo, interfaceIBar)
+
+ private fun createVisitedPath(filename: String) =
+ "src/frameworks/base/services/java/com/android/server/$filename"
+
+ private val ignoredPath = "src/test/pkg/TestClass.java"
+}
diff --git a/tools/lint/utils/generate-exempt-aidl-interfaces.sh b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
new file mode 100755
index 0000000..44dcdd7
--- /dev/null
+++ b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
@@ -0,0 +1,59 @@
+#
+# 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.
+#
+
+# Create a directory for the results and a nested temporary directory.
+mkdir -p $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Create a copy of `AndroidGlobalLintChecker.jar` to restore it afterwards.
+cp $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar \
+ $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar
+
+# Configure the environment variable required for running the lint check on the entire source tree.
+export ANDROID_LINT_CHECK=PermissionAnnotationExemptAidlInterfaces
+
+# Build the target corresponding to the lint checks present in the `utils` directory.
+m AndroidUtilsLintChecker
+
+# Replace `AndroidGlobalLintChecker.jar` with the newly built `jar` file.
+cp $ANDROID_BUILD_TOP/out/host/linux-x86/framework/AndroidUtilsLintChecker.jar \
+ $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar;
+
+# Run the lint check on the entire source tree.
+m lint-check
+
+# Copy the archive containing the results of `lint-check` into the temporary directory.
+cp $ANDROID_BUILD_TOP/out/soong/lint-report-text.zip \
+ $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+cd $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Unzip the archive containing the results of `lint-check`.
+unzip lint-report-text.zip
+
+# Concatenate the results of `lint-check` into a single string.
+concatenated_reports=$(find . -type f | xargs cat)
+
+# Extract the fully qualified names of the AIDL Interfaces from the concatenated results. Output
+# this list into `out/soong/exempt_aidl_interfaces_generator_output/exempt_aidl_interfaces`.
+echo $concatenated_reports | grep -Eo '\"([a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]*\",' | sort | uniq > ../exempt_aidl_interfaces
+
+# Remove the temporary directory.
+rm -rf $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Restore the original copy of `AndroidGlobalLintChecker.jar` and delete the copy.
+cp $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar \
+ $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar
+rm $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar